diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357a7..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..1f72dcaf8 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,19 @@ +import { defineConfig } from 'eslint/config' +import nextCoreWebVitals from 'eslint-config-next/core-web-vitals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export default defineConfig([ + { + extends: [...nextCoreWebVitals], + rules: { + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-compiler/react-compiler': 'off', + }, + }, +]) diff --git a/next.config.js b/next.config.js index 5fa2fe9fe..244e21663 100644 --- a/next.config.js +++ b/next.config.js @@ -18,6 +18,14 @@ const nextConfig = { return config }, + turbopack: { + rules: { + '*.svg': { + loaders: ['@svgr/webpack'], + as: '*.jsx', + }, + }, + }, } module.exports = withSentryConfig(nextConfig, { diff --git a/package.json b/package.json index c1f35df2a..1af40c850 100644 --- a/package.json +++ b/package.json @@ -29,26 +29,27 @@ "jsdom": "^25.0.1", "localforage": "^1.10.0", "marked": "^16.3.0", - "next": "14.2.35", + "next": "16.1.1", "nextjs-progressloader": "^1.2.0", "p-retry": "^6.2.0", "prisma": "^5.19.0", - "react": "^18", + "react": "19.2.3", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dnd-touch-backend": "^16.0.1", - "react-dom": "^18.3", + "react-dom": "19.2.3", "react-redux": "^9.1.0", "react-zoom-pan-pinch": "^3.7.0", "server-only": "^0.0.1", "supabase": "^2.1.1", "swr": "^2.2.5", - "tapwrite": "1.1.93", + "tapwrite": "1.1.94", "uuid": "^11.1.0", "zod": "^3.23.8" }, "description": "A comprehensive task management app", "devDependencies": { + "@eslint/eslintrc": "^3.3.3", "@faker-js/faker": "^8.4.1", "@ngrok/ngrok": "^1.4.1", "@svgr/webpack": "^8.1.0", @@ -57,18 +58,19 @@ "@types/jest": "^29.5.12", "@types/jsdom": "^21.1.7", "@types/node": "^20", - "@types/react": "^18", + "@types/react": "19.2.7", "@types/react-custom-scrollbars": "^4.0.13", - "@types/react-dom": "^18", + "@types/react-dom": "19.2.3", "autoprefixer": "^10.0.1", - "eslint": "^8", - "eslint-config-next": "14.0.4", + "eslint": "^9", + "eslint-config-next": "16.1.1", "husky": "^8.0.0", "jest": "^29.7.0", "nodemon": "^3.1.9", "open": "^10.1.0", "prettier": "^3.1.1", "tailwindcss": "^3.3.0", + "text-table": "^0.2.0", "tsx": "^4.16.5", "typescript": "^5" }, diff --git a/sentry.client.config.ts b/sentry.client.config.ts index 4d755b578..a5d0e6fac 100644 --- a/sentry.client.config.ts +++ b/sentry.client.config.ts @@ -2,47 +2,55 @@ // 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 * as Sentry from '@sentry/nextjs' +import * as Sentry from "@sentry/nextjs"; -const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN -const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV -const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === 'production' +const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN; +const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV; +const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; if (dsn) { - Sentry.init({ - dsn, - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: isProd ? 0.2 : 1, - profilesSampleRate: 0.1, - // NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data - - // Setting this option to true will print useful information to the console while you're setting up Sentry. - debug: false, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - // NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least - // replaysOnErrorSampleRate: 1.0, - // replaysSessionSampleRate: 0, - integrations: [ - Sentry.browserTracingIntegration(), - // Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - // maskAllText: true, - // blockAllMedia: true, - // }), - ], - - // ignoreErrors: [/fetch failed/i], - ignoreErrors: [/fetch failed/i], - - beforeSend(event) { - event.tags = { - ...event.tags, - // Adding additional app_env tag for cross-checking - app_env: isProd ? 'production' : vercelEnv || 'development', - } - return event - }, - }) + Sentry.init({ + dsn, + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: isProd ? 0.2 : 1, + profilesSampleRate: 0.1, + // NOTE: reducing sample only 10% of transactions in prod to get general trends instead of detailed and overfitted data + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + // NOTE: Since session replay barely helps us anyways, getting rid of it to reduce some bundle size at least + // replaysOnErrorSampleRate: 1.0, + // replaysSessionSampleRate: 0, + integrations: [ + Sentry.browserTracingIntegration({ + beforeStartSpan: (e) => { + console.info("SentryBrowserTracingSpan", e.name); + return e; + }, + }), + // Sentry.replayIntegration({ + // Additional Replay configuration goes in here, for example: + // maskAllText: true, + // blockAllMedia: true, + // }), + ], + + // ignoreErrors: [/fetch failed/i], + ignoreErrors: [/fetch failed/i], + + beforeSend(event) { + if (!isProd && event.type === undefined) { + return null; + } + event.tags = { + ...event.tags, + // Adding additional app_env tag for cross-checking + app_env: isProd ? "production" : vercelEnv || "development", + }; + return event; + }, + }); } diff --git a/sentry.server.config.ts b/sentry.server.config.ts index fee202c8c..174077b7b 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -2,22 +2,31 @@ // The config you add here will be used whenever the server handles a request. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ -import * as Sentry from '@sentry/nextjs' +import * as Sentry from "@sentry/nextjs"; -const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN +const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN; +const vercelEnv = process.env.NEXT_PUBLIC_VERCEL_ENV; +const isProd = process.env.NEXT_PUBLIC_VERCEL_ENV === "production"; if (dsn) { - Sentry.init({ - dsn, + Sentry.init({ + dsn, - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 1, + // 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, + // 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', - ignoreErrors: [/fetch failed/i], - }) + // Uncomment the line below to enable Spotlight (https://spotlightjs.com) + // spotlight: process.env.NODE_ENV === 'development', + ignoreErrors: [/fetch failed/i], + + beforeSend(event) { + if (!isProd && event.type === undefined) { + return null; + } + return event; + }, + }); } diff --git a/src/app/(home)/page.tsx b/src/app/(home)/page.tsx index 55c8a8f07..0787591f3 100644 --- a/src/app/(home)/page.tsx +++ b/src/app/(home)/page.tsx @@ -74,11 +74,10 @@ export async function getViewSettings(token: string): Promise }) { + const searchParams = await props.searchParams const token = searchParams.token const parsedToken = z.string().safeParse(searchParams.token) diff --git a/src/app/_fetchers/OneTaskDataFetcher.tsx b/src/app/_fetchers/OneTaskDataFetcher.tsx index 852c8faa3..c8f6c3b6b 100644 --- a/src/app/_fetchers/OneTaskDataFetcher.tsx +++ b/src/app/_fetchers/OneTaskDataFetcher.tsx @@ -15,7 +15,6 @@ interface OneTaskDataFetcherProps extends PropsWithToken { } export const OneTaskDataFetcher = ({ token, task_id, initialTask }: OneTaskDataFetcherProps & PropsWithToken) => { - const hasDispatchedRef = useRef(false) const buildQueryString = (token: string) => { const queryParams = new URLSearchParams({ token }) @@ -30,7 +29,7 @@ export const OneTaskDataFetcher = ({ token, task_id, initialTask }: OneTaskDataF }) useEffect(() => { - if (!hasDispatchedRef.current && data?.task) { + if (data?.task) { //only invalidate cache on mount. const newTask = structuredClone(data.task) if (initialTask?.body && newTask.body === undefined) { @@ -44,7 +43,6 @@ export const OneTaskDataFetcher = ({ token, task_id, initialTask }: OneTaskDataF } } store.dispatch(setActiveTask(newTask)) - hasDispatchedRef.current = true } }, [data]) diff --git a/src/app/api/activity-logs/[id]/route.ts b/src/app/api/activity-logs/[id]/route.ts index 446ff2007..a6d506f7a 100644 --- a/src/app/api/activity-logs/[id]/route.ts +++ b/src/app/api/activity-logs/[id]/route.ts @@ -4,7 +4,11 @@ import { ActivityLogService } from '@api/activity-logs/services/activity-log.ser import { IdParams } from '@api/core/types/api' import { unstable_noStore as noStore } from 'next/cache' -export const GET = async (req: NextRequest, { params: { id } }: IdParams) => { +export const GET = async (req: NextRequest, props: IdParams) => { + const params = await props.params + + const { id } = params + noStore() const user = await authenticate(req) diff --git a/src/app/api/attachments/attachments.controller.ts b/src/app/api/attachments/attachments.controller.ts index 40b51775b..6ef717e1f 100644 --- a/src/app/api/attachments/attachments.controller.ts +++ b/src/app/api/attachments/attachments.controller.ts @@ -40,7 +40,8 @@ export const getAttachments = async (req: NextRequest) => { return NextResponse.json({ attachments }) } -export const deleteAttachment = async (req: NextRequest, { params: { id } }: IdParams) => { +export const deleteAttachment = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const attachmentsService = new AttachmentsService(user) await attachmentsService.deleteAttachment(id) diff --git a/src/app/api/comment/comment.controller.ts b/src/app/api/comment/comment.controller.ts index 015c5f963..ea234fe52 100755 --- a/src/app/api/comment/comment.controller.ts +++ b/src/app/api/comment/comment.controller.ts @@ -17,7 +17,8 @@ export const createComment = async (req: NextRequest) => { return NextResponse.json({ comment }, { status: httpStatus.CREATED }) } -export const deleteComment = async (req: NextRequest, { params: { id } }: IdParams) => { +export const deleteComment = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const commentService = new CommentService(user) @@ -27,7 +28,8 @@ export const deleteComment = async (req: NextRequest, { params: { id } }: IdPara return NextResponse.json({ message: 'Comment deleted!' }) } -export const updateComment = async (req: NextRequest, { params: { id } }: IdParams) => { +export const updateComment = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const data = UpdateCommentSchema.parse(await req.json()) diff --git a/src/app/api/core/types/api.ts b/src/app/api/core/types/api.ts index 24c331172..6acc0d6cc 100644 --- a/src/app/api/core/types/api.ts +++ b/src/app/api/core/types/api.ts @@ -17,7 +17,5 @@ export enum Resource { * NextParam when uuid is being used as a dynamic param (slug) for accessing a resource */ export type IdParams = { - params: { - id: string - } + params: Promise<{ id: string }> } diff --git a/src/app/api/core/utils/withRetry.ts b/src/app/api/core/utils/withRetry.ts index 2ac2638ea..dbc9a2d4f 100644 --- a/src/app/api/core/utils/withRetry.ts +++ b/src/app/api/core/utils/withRetry.ts @@ -2,6 +2,8 @@ import { StatusableError } from '@/types/CopilotApiError' import pRetry, { FailedAttemptError } from 'p-retry' import * as Sentry from '@sentry/nextjs' +export const RETRY_404_ENABLED = process.env.RETRY_404 === 'true' + export const withRetry = async (fn: (...args: any[]) => Promise, args: any[]): Promise => { let isEventProcessorRegistered = false @@ -41,7 +43,11 @@ export const withRetry = async (fn: (...args: any[]) => Promise, args: any // Typecasting because Copilot doesn't export an error class const err = error as StatusableError // Retry if statusCode is 429 (ratelimit), 408 (timeouts), or any server related (5xx) error - return [408, 429].includes(err.status) || (err.status >= 500 && err.status <= 511) + return ( + [408, 429].includes(err.status) || + (err.status >= 500 && err.status <= 511) || + (RETRY_404_ENABLED && err.status === 404) + ) }, }, ) diff --git a/src/app/api/tasks/[id]/activity-logs/activity.controller.ts b/src/app/api/tasks/[id]/activity-logs/activity.controller.ts index 48e0647c8..98dac39f0 100644 --- a/src/app/api/tasks/[id]/activity-logs/activity.controller.ts +++ b/src/app/api/tasks/[id]/activity-logs/activity.controller.ts @@ -5,7 +5,8 @@ import authenticate from '@api/core/utils/authenticate' import httpStatus from 'http-status' import { NextRequest, NextResponse } from 'next/server' -export const get = async (req: NextRequest, { params: { id } }: IdParams) => { +export const get = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const { expandComments } = getSearchParams(req.nextUrl.searchParams, ['expandComments']) diff --git a/src/app/api/tasks/public/public.controller.ts b/src/app/api/tasks/public/public.controller.ts index 457daa0a2..0638e7fde 100644 --- a/src/app/api/tasks/public/public.controller.ts +++ b/src/app/api/tasks/public/public.controller.ts @@ -9,6 +9,7 @@ import { TasksService } from '@api/tasks/tasks.service' import { decode, encode } from 'js-base64' import { NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { PublicTasksService } from '@api/tasks/public/public.service' export const getAllTasksPublic = async (req: NextRequest) => { const user = await authenticate(req) @@ -31,9 +32,8 @@ export const getAllTasksPublic = async (req: NextRequest) => { workflowState: workflowStateType && { type: workflowStateType }, } - const tasksService = new TasksService(user) + const tasksService = new PublicTasksService(user) const tasks = await tasksService.getAllTasks({ - fromPublicApi: true, showArchived: true, showUnarchived: true, all: !parentTaskId, @@ -52,10 +52,11 @@ export const getAllTasksPublic = async (req: NextRequest) => { }) } -export const getOneTaskPublic = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getOneTaskPublic = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) - const tasksService = new TasksService(user) - const task = await tasksService.getOneTask(id, true) //from public API is true + const tasksService = new PublicTasksService(user) + const task = await tasksService.getOneTask(id) return NextResponse.json(PublicTaskSerializer.serialize(task)) } @@ -68,28 +69,30 @@ export const createTaskPublic = async (req: NextRequest) => { const createPayload = await PublicTaskSerializer.deserializeCreatePayload(data, user.workspaceId) console.info('Deserialized create payload:', createPayload) - const tasksService = new TasksService(user) - const newTask = await tasksService.createTask(createPayload, { isPublicApi: true }) + const tasksService = new PublicTasksService(user) + const newTask = await tasksService.createTask(createPayload) console.info('Created new public task:', newTask) return NextResponse.json(PublicTaskSerializer.serialize(newTask)) } -export const updateTaskPublic = async (req: NextRequest, { params: { id } }: IdParams) => { +export const updateTaskPublic = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const data = PublicTaskUpdateDtoSchema.parse(await req.json()) - const tasksService = new TasksService(user) + const tasksService = new PublicTasksService(user) const updatePayload = await PublicTaskSerializer.deserializeUpdatePayload(data, user.workspaceId) - const updatedTask = await tasksService.updateOneTask(id, updatePayload, { isPublicApi: true }) + const updatedTask = await tasksService.updateTask(id, updatePayload) return NextResponse.json(PublicTaskSerializer.serialize(updatedTask)) } -export const deleteOneTaskPublic = async (req: NextRequest, { params: { id } }: IdParams) => { +export const deleteOneTaskPublic = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const recursive = req.nextUrl.searchParams.get('recursive') const user = await authenticate(req) - const tasksService = new TasksService(user) - const task = await tasksService.deleteOneTask(id, z.coerce.boolean().parse(recursive)) + const tasksService = new PublicTasksService(user) + const task = await tasksService.deleteTask(id, z.coerce.boolean().parse(recursive)) return NextResponse.json({ ...PublicTaskSerializer.serialize(task) }) } diff --git a/src/app/api/tasks/public/public.service.ts b/src/app/api/tasks/public/public.service.ts new file mode 100644 index 000000000..f4f3745f7 --- /dev/null +++ b/src/app/api/tasks/public/public.service.ts @@ -0,0 +1,463 @@ +import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users' +import { deleteTaskNotifications, sendTaskCreateNotifications, sendTaskUpdateNotifications } from '@/jobs/notifications' +import { TaskWithWorkflowState } from '@/types/db' +import { CreateTaskRequest, CreateTaskRequestSchema, UpdateTaskRequest, Viewers, ViewersSchema } from '@/types/dto/tasks.dto' +import { DISPATCHABLE_EVENT } from '@/types/webhook' +import { UserIdsType } from '@/utils/assignee' +import { isPastDateString } from '@/utils/dateHelper' +import { PoliciesService } from '@api/core/services/policies.service' +import { Resource } from '@api/core/types/api' +import { UserAction } from '@api/core/types/user' +import { + dispatchUpdatedWebhookEvent, + getArchivedStatus, + getTaskTimestamps, + queueBodyUpdatedWebhook, +} from '@api/tasks/tasks.helpers' +import { TasksSharedService } from '@/app/api/tasks/tasksShared.service' +import { AssigneeType, Prisma, PrismaClient, Source, StateType, Task, TaskTemplate, WorkflowState } from '@prisma/client' +import httpStatus from 'http-status' +import z from 'zod' +import APIError from '@api/core/exceptions/api' +import { LabelMappingService } from '@api/label-mapping/label-mapping.service' +import { SubtaskService } from '@api/tasks/subtasks.service' +import { TasksActivityLogger } from '@api/tasks/tasks.logger' +import { TemplatesService } from '@api/tasks/templates/templates.service' +import { PublicTaskSerializer } from '@api/tasks/public/public.serializer' + +export class PublicTasksService extends TasksSharedService { + async getAllTasks(queryFilters: { + all?: boolean + showIncompleteOnly?: boolean + showArchived: boolean + showUnarchived: boolean + internalUserId?: string + clientId?: string + companyId?: string + createdById?: string + parentId?: string | null + workflowState?: { type: StateType | { not: StateType } } + limit?: number + lastIdCursor?: string + }): Promise { + const policyGate = new PoliciesService(this.user) + policyGate.authorize(UserAction.Read, Resource.Tasks) + + const filters: Prisma.TaskWhereInput = this.buildTaskPermissions() + + let isArchived: boolean | undefined = false + + if (queryFilters.all) { + isArchived = undefined + } else { + // Archived tasks are only accessible to IU + // If both archived filters are explicitly 0 / falsey for IU, shortcircuit and return empty array + if (!queryFilters.showArchived && !queryFilters.showUnarchived) { + return [] + } + isArchived = getArchivedStatus(queryFilters.showArchived, queryFilters.showUnarchived) + } + + // If `parentId` is present, filter by parentId, ELSE return top-level parent comments + filters.parentId = queryFilters.all + ? undefined // if querying all accessible tasks, parentId filter doesn't make sense + : await this.getParentIdFilter(queryFilters.parentId) + + const disjointTasksFilter: Prisma.TaskWhereInput = + queryFilters.all || queryFilters.parentId + ? {} // No need to support disjoint tasks when querying all tasks / subtasks + : await this.getDisjointTasksFilter() + + const where: Prisma.TaskWhereInput = { + ...filters, + ...disjointTasksFilter, + internalUserId: queryFilters.internalUserId, + clientId: queryFilters.clientId, + companyId: queryFilters.companyId, + createdById: queryFilters.createdById, + workflowState: queryFilters.workflowState || filters.workflowState, + isArchived, + } + + const orderBy: Prisma.TaskOrderByWithRelationInput[] = [{ createdAt: 'desc' }] + const pagination: Prisma.TaskFindManyArgs = { + take: queryFilters.limit, + cursor: queryFilters.lastIdCursor ? { id: queryFilters.lastIdCursor } : undefined, + skip: queryFilters.lastIdCursor ? 1 : undefined, + } + + const tasks = await this.db.task.findMany({ + where, + orderBy, + ...pagination, + relationLoadStrategy: 'join', + include: { workflowState: true }, + }) + + return tasks + } + + async getOneTask(id: string): Promise { + const policyGate = new PoliciesService(this.user) + policyGate.authorize(UserAction.Read, Resource.Tasks) + + // Build query filters based on role of user. IU can access all tasks related to a workspace + // while clients can only view the tasks assigned to them or their company + const filters = this.buildTaskPermissions(id) + const where = { ...filters, deletedAt: { not: undefined } } + const task = await this.db.task.findFirst({ where, relationLoadStrategy: 'join', include: { workflowState: true } }) + if (!task) throw new APIError(httpStatus.NOT_FOUND, 'The requested task was not found') + if (this.user.internalUserId) { + await this.checkClientAccessForTask(task, this.user.internalUserId) + } + return task + } + + async createTask(data: CreateTaskRequest, opts?: { disableSubtaskTemplates?: boolean; manualTimestamp?: Date }) { + const policyGate = new PoliciesService(this.user) + policyGate.authorize(UserAction.Create, Resource.Tasks) + console.info('PublicTasksService#createTask | Creating task from public api with data:', data) + + const { internalUserId, clientId, companyId } = data + + const validatedIds = await this.validateUserIds(internalUserId, clientId, companyId) + console.info('PublicTasksService#createTask | Validated user IDs:', validatedIds) + + const { assigneeId, assigneeType } = this.getAssigneeFromUserIds({ + internalUserId: validatedIds.internalUserId, + clientId: validatedIds.clientId, + companyId: validatedIds.companyId, + }) + + //generate the label + const labelMappingService = new LabelMappingService(this.user) + const label = z.string().parse(await labelMappingService.getLabel(validatedIds)) + console.info('PublicTasksService#createTask | Generated label for task:', label) + if (data.parentId) { + const canCreateSubTask = await this.canCreateSubTask(data.parentId) + if (!canCreateSubTask) { + throw new APIError(httpStatus.BAD_REQUEST, 'Reached the maximum subtask depth for this task') + } + } + console.info('PublicTasksService#createTask | Subtask depth validated for parentId:', data.parentId) + + if (data.dueDate && isPastDateString(data.dueDate)) { + throw new APIError(httpStatus.BAD_REQUEST, 'Due date cannot be in the past') + } + + const { completedBy, completedByUserType, workflowStateStatus } = await this.getCompletionInfo(data?.workflowStateId) + console.info('PublicTasksService#createTask | Completion info determined:', { completedBy, completedByUserType }) + + // NOTE: This block strictly doesn't allow clients to create tasks + let createdById = z.string().parse(this.user.internalUserId) + + if (data.createdById) { + const internalUsers = await this.copilot.getInternalUsers({ limit: MAX_FETCH_ASSIGNEE_COUNT }) // there are 2 of these api running on this same serive. need to think about this later. + const createdBy = internalUsers?.data?.find((iu) => iu.id === data.createdById) + if (!createdBy) { + throw new APIError(httpStatus.BAD_REQUEST, 'The requested user for createdBy was not found') + } + createdById = createdBy.id + console.info('TasksService#createTask | createdById overridden for public API:', createdById) + } + + let viewers: Viewers = [] + if (data.viewers?.length) { + if (!validatedIds.internalUserId) { + throw new APIError(httpStatus.BAD_REQUEST, `Task cannot be created with viewers if its not assigned to an IU.`) + } + viewers = await this.validateViewers(data.viewers) + console.info('PublicTasksService#createTask | Viewers validated for task:', viewers) + } + + // Create a new task associated with current workspaceId. Also inject current request user as the creator. + const newTask = await this.db.task.create({ + data: { + ...data, + workspaceId: this.user.workspaceId, + createdById, + label: label, + completedBy, + completedByUserType, + source: Source.api, + assigneeId, + assigneeType, + viewers: viewers, + ...validatedIds, + ...(opts?.manualTimestamp && { createdAt: opts.manualTimestamp }), + ...(await getTaskTimestamps('create', this.user, data, undefined, workflowStateStatus)), + }, + include: { workflowState: true }, + }) + console.info('PublicTasksService#createTask | Task created with ID:', newTask.id) + + if (newTask) { + // Add activity logs + const activityLogger = new TasksActivityLogger(this.user, newTask) + await activityLogger.logNewTask({ + userId: createdById, + role: AssigneeType.internalUser, + }) //hardcoding internalUser as role since task can only be created by IUs. + console.info('TasksService#createTask | Activity log created for new task ID:', newTask.id) + + try { + if (newTask.body) { + const newBody = await this.updateTaskIdOfAttachmentsAfterCreation(newTask.body, newTask.id) + // Update task body with replaced attachment sources + await this.db.task.update({ + where: { id: newTask.id }, + data: { + body: newBody, + }, + }) + console.info('TasksService#createTask | Task body attachments updated for task ID:', newTask.id) + } + + // Add ltree path for task + await this.addPathToTask(newTask) + + // Increment parent task's subtask count, if exists + if (newTask.parentId) { + const subtaskService = new SubtaskService(this.user) + await Promise.all([ + subtaskService.addSubtaskCount(newTask.parentId), + this.setNewLastSubtaskUpdated(newTask.parentId), + ]) + } + console.info('PublicTasksService#createTask | Post-processing completed for task ID:', newTask.id) + } catch (e: unknown) { + // Manually rollback task creation + await this.db.$transaction([ + this.db.task.delete({ where: { id: newTask.id } }), + this.db.activityLog.deleteMany({ where: { taskId: newTask.id } }), + ]) + console.error('TasksService#createTask | Rolling back task creation', e) + throw new APIError(httpStatus.INTERNAL_SERVER_ERROR, 'Failed to post-process task, new task was not created.') + } + } + + // Send task created notifications to users + dispatch webhook + await Promise.all([ + sendTaskCreateNotifications.trigger({ user: this.user, task: newTask }), + this.copilot.dispatchWebhook(DISPATCHABLE_EVENT.TaskCreated, { + payload: PublicTaskSerializer.serialize(newTask), + workspaceId: this.user.workspaceId, + }), + ]) + + if (data.templateId) { + const templateService = new TemplatesService(this.user) + const template = await templateService.getOneTemplate(data.templateId) + + if (!template) { + throw new APIError(httpStatus.NOT_FOUND, 'The requested template was not found') + } + + if (template.subTaskTemplates.length) { + await Promise.all( + template.subTaskTemplates.map(async (sub, index) => { + const updatedSubTemplate = await templateService.getAppliedTemplateDescription(sub.id) + const manualTimeStamp = new Date(template.createdAt.getTime() + (template.subTaskTemplates.length - index) * 10) //maintain the order of subtasks in tasks with respect to subtasks in templates + await this.createSubtasksFromTemplate(updatedSubTemplate, newTask, manualTimeStamp) + }), + ) + } + } + + return newTask + } + + async updateTask(id: string, data: UpdateTaskRequest) { + const policyGate = new PoliciesService(this.user) + policyGate.authorize(UserAction.Update, Resource.Tasks) + const filters = this.buildTaskPermissions(id) + + const prevTask = await this.db.task.findFirst({ + where: filters, + relationLoadStrategy: 'join', + include: { workflowState: true }, + }) + + if (!prevTask) throw new APIError(httpStatus.NOT_FOUND, 'The requested task was not found') + + const { completedBy, completedByUserType } = await this.getCompletionInfo(data?.workflowStateId) + + const { internalUserId, clientId, companyId, ...dataWithoutUserIds } = data + + //todo : keep this in a separate shared util + const shouldUpdateUserIds = + (internalUserId !== undefined && internalUserId !== prevTask?.internalUserId) || + (clientId !== undefined && clientId !== prevTask?.clientId) || + (companyId !== undefined && companyId !== prevTask?.companyId) + + let validatedIds: UserIdsType | undefined + + if (shouldUpdateUserIds) { + validatedIds = await this.validateUserIds(internalUserId, clientId, companyId) + } + + const { assigneeId, assigneeType } = this.getAssigneeFromUserIds({ + internalUserId: validatedIds?.internalUserId ?? null, + clientId: validatedIds?.clientId ?? null, + companyId: validatedIds?.companyId ?? null, + }) + + let viewers: Viewers = ViewersSchema.parse(prevTask.viewers) + + const viewersResetCondition = shouldUpdateUserIds ? !!clientId || !!companyId : !prevTask.internalUserId + if (data.viewers) { + // only update of viewers attribute is available. No viewers in payload attribute means the data remains as it is in DB. + if (viewersResetCondition || !data.viewers?.length) { + viewers = [] // reset viewers to [] if task is not reassigned to IU. + } else if (data.viewers?.length) { + viewers = await this.validateViewers(data.viewers) + } + } + + const userAssignmentFields = shouldUpdateUserIds + ? { + ...validatedIds, + assigneeId, + assigneeType, + } + : {} + + // Validate updated due date to not be in the past + if (data.dueDate && isPastDateString(data.dueDate)) { + throw new APIError(httpStatus.BAD_REQUEST, 'Due date cannot be in the past') + } + const subtaskService = new SubtaskService(this.user) + + let updatedTask = await this.db.$transaction(async (tx) => { + //generate new label if prevTask has no assignee but now assigned to someone + let label: string = prevTask.label + if (!prevTask.assigneeId && assigneeId && assigneeType) { + const labelMappingService = new LabelMappingService(this.user) + labelMappingService.setTransaction(tx as PrismaClient) + //delete the existing label + await labelMappingService.deleteLabel(prevTask.label) + if (validatedIds) { + label = z.string().parse(await labelMappingService.getLabel(validatedIds)) + } + } + + // Set / reset lastArchivedDate if isArchived has been triggered, else remove it from the update query + let lastArchivedDate: Date | undefined | null = undefined + let archivedBy: string | null | undefined = undefined + if (data.isArchived !== undefined && prevTask.isArchived !== data.isArchived) { + lastArchivedDate = data.isArchived === true ? new Date() : data.isArchived === false ? null : undefined + archivedBy = data.isArchived === true ? this.user.internalUserId : data.isArchived === false ? null : undefined + } + + // Get the updated task + const updatedTask = await tx.task.update({ + where: { id }, + data: { + ...dataWithoutUserIds, + label, + lastArchivedDate, + archivedBy, + completedBy, + completedByUserType, + viewers, + ...userAssignmentFields, + ...(await getTaskTimestamps('update', this.user, data, prevTask)), + }, + include: { workflowState: true }, + }) + subtaskService.setTransaction(tx as PrismaClient) + // Archive / unarchive all subtasks if parent task is archived / unarchived + if (prevTask.isArchived !== data.isArchived && data.isArchived !== undefined) { + await subtaskService.toggleArchiveForAllSubtasks(id, data.isArchived) + } + + return updatedTask + }) + + if (updatedTask) { + const activityLogger = new TasksActivityLogger(this.user, updatedTask) + const isBodyChanged = prevTask.body !== updatedTask.body + await Promise.all([ + activityLogger.logTaskUpdated(prevTask), + this.setNewLastSubtaskUpdated(updatedTask.parentId), + sendTaskUpdateNotifications.trigger({ prevTask, updatedTask, user: this.user }), + dispatchUpdatedWebhookEvent(this.user, prevTask, updatedTask, true), + isBodyChanged ? queueBodyUpdatedWebhook(this.user, updatedTask) : undefined, + ]) + } + + return updatedTask + } + + async deleteTask(id: string, recursive: boolean = true) { + const policyGate = new PoliciesService(this.user) + policyGate.authorize(UserAction.Delete, Resource.Tasks) + const deletedBy = this.user.internalUserId + + // Try to delete existing client notification related to this task if exists + const task = await this.db.task.findFirst({ + where: { id, workspaceId: this.user.workspaceId }, + relationLoadStrategy: 'join', + include: { workflowState: true }, + }) + + if (!task) throw new APIError(httpStatus.NOT_FOUND, 'The requested task to delete was not found') + + if (!recursive) { + if (task.subtaskCount > 0) { + throw new APIError(httpStatus.CONFLICT, 'Cannot delete task with subtasks. Use recursive delete instead.') + } + } + + //delete the associated label + const labelMappingService = new LabelMappingService(this.user) + + const updatedTask = await this.db.$transaction(async (tx) => { + labelMappingService.setTransaction(tx as PrismaClient) + await labelMappingService.deleteLabel(task?.label) + + const deletedTask = await tx.task.update({ + where: { id, workspaceId: this.user.workspaceId }, + relationLoadStrategy: 'join', + include: { workflowState: true }, + data: { deletedAt: new Date(), deletedBy: deletedBy }, + }) + await this.setNewLastSubtaskUpdated(task.parentId) //updates lastSubtaskUpdated timestamp of parent task if there is task.parentId + const subtaskService = new SubtaskService(this.user) + subtaskService.setTransaction(tx as PrismaClient) + if (task.parentId) { + await subtaskService.decreaseSubtaskCount(task.parentId) + } + await subtaskService.softDeleteAllSubtasks(task.id) + return deletedTask + }) + + await Promise.all([ + deleteTaskNotifications.trigger({ user: this.user, task }), + this.copilot.dispatchWebhook(DISPATCHABLE_EVENT.TaskDeleted, { + payload: PublicTaskSerializer.serialize(updatedTask), + workspaceId: this.user.workspaceId, + }), + ]) + + return updatedTask + + // Logic to remove internal user notifications when a task is deleted / assignee is deleted + // ...In case requirements change later again + // const notificationService = new NotificationService(this.user) + // await notificationService.deleteInternalUserNotificationForTask(id) + } + + async hasMoreTasksAfterCursor( + id: string, + publicFilters: Partial[0]>, + ): Promise { + const nextTask = await this.db.task.findFirst({ + where: { ...publicFilters, workspaceId: this.user.workspaceId }, + cursor: { id }, + skip: 1, + orderBy: { createdAt: 'desc' }, + }) + return !!nextTask + } +} diff --git a/src/app/api/tasks/subtasks.controller.ts b/src/app/api/tasks/subtasks.controller.ts index d7bb69c99..0cc64f0e2 100644 --- a/src/app/api/tasks/subtasks.controller.ts +++ b/src/app/api/tasks/subtasks.controller.ts @@ -3,7 +3,8 @@ import authenticate from '@api/core/utils/authenticate' import { SubtaskService } from '@api/tasks/subtasks.service' import { NextRequest, NextResponse } from 'next/server' -export const getSubtaskCount = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getSubtaskCount = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const subtaskService = new SubtaskService(user) const count = await subtaskService.getSubtaskCounts(id) diff --git a/src/app/api/tasks/task-notifications.service.ts b/src/app/api/tasks/task-notifications.service.ts index a235d22a7..a49b72e10 100644 --- a/src/app/api/tasks/task-notifications.service.ts +++ b/src/app/api/tasks/task-notifications.service.ts @@ -274,6 +274,9 @@ export class TaskNotificationsService extends BaseService { } catch (e: unknown) { console.error(`Failed to find ClientNotification for task ${updatedTask.id}`, e) } + } else if (updatedTask.assigneeType === AssigneeType.internalUser) { + shouldCreateNotification && + (await this.notificationService.create(NotificationTaskActions.CompletedByIU, updatedTask, { disableEmail: true })) } } diff --git a/src/app/api/tasks/tasks.controller.ts b/src/app/api/tasks/tasks.controller.ts index 413a3cb1a..b5972d02b 100644 --- a/src/app/api/tasks/tasks.controller.ts +++ b/src/app/api/tasks/tasks.controller.ts @@ -49,7 +49,8 @@ export const createTask = async (req: NextRequest) => { return NextResponse.json(newTask, { status: httpStatus.CREATED }) } -export const getTask = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getTask = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const tasksService = new TasksService(user) const task = await tasksService.getOneTask(id) @@ -57,7 +58,8 @@ export const getTask = async (req: NextRequest, { params: { id } }: IdParams) => return NextResponse.json({ task: { ...task, assignee } }) } -export const updateTask = async (req: NextRequest, { params: { id } }: IdParams) => { +export const updateTask = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const data = UpdateTaskRequestSchema.parse(await req.json()) @@ -67,7 +69,8 @@ export const updateTask = async (req: NextRequest, { params: { id } }: IdParams) return NextResponse.json({ updatedTask }) } -export const deleteTask = async (req: NextRequest, { params: { id } }: IdParams) => { +export const deleteTask = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const tasksService = new TasksService(user) @@ -75,7 +78,8 @@ export const deleteTask = async (req: NextRequest, { params: { id } }: IdParams) return new NextResponse(null, { status: httpStatus.NO_CONTENT }) } -export const clientUpdateTask = async (req: NextRequest, { params: { id } }: IdParams) => { +export const clientUpdateTask = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const workflowStateId = req.nextUrl.searchParams.get('workflowStateId') const tasksService = new TasksService(user) @@ -83,7 +87,8 @@ export const clientUpdateTask = async (req: NextRequest, { params: { id } }: IdP return NextResponse.json({ updatedTask }) } -export const getTaskPath = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getTaskPath = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const tasksService = new TasksService(user) const path = await tasksService.getTraversalPath(id) diff --git a/src/app/api/tasks/tasks.service.ts b/src/app/api/tasks/tasks.service.ts index ce2965bec..4c1eab127 100644 --- a/src/app/api/tasks/tasks.service.ts +++ b/src/app/api/tasks/tasks.service.ts @@ -1,8 +1,6 @@ -import { maxSubTaskDepth } from '@/constants/tasks' -import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users' import { deleteTaskNotifications, sendTaskCreateNotifications, sendTaskUpdateNotifications } from '@/jobs/notifications' import { sendClientUpdateTaskNotifications } from '@/jobs/notifications/send-client-task-update-notifications' -import { ClientResponse, CompanyResponse, InternalUsers, Uuid } from '@/types/common' +import { ClientResponse, CompanyResponse, InternalUsers } from '@/types/common' import { TaskWithWorkflowState } from '@/types/db' import { AncestorTaskResponse, @@ -15,59 +13,30 @@ import { import { DISPATCHABLE_EVENT } from '@/types/webhook' import { UserIdsType } from '@/utils/assignee' import { isPastDateString } from '@/utils/dateHelper' -import { buildLtree, buildLtreeNodeString, getIdsFromLtreePath } from '@/utils/ltree' -import { getFilePathFromUrl, replaceImageSrc } from '@/utils/signedUrlReplacer' +import { getIdsFromLtreePath } from '@/utils/ltree' +import { replaceImageSrc } from '@/utils/signedUrlReplacer' import { getSignedUrl } from '@/utils/signUrl' -import { SupabaseActions } from '@/utils/SupabaseActions' import APIError from '@api/core/exceptions/api' -import { BaseService } from '@api/core/services/base.service' import { PoliciesService } from '@api/core/services/policies.service' import { Resource } from '@api/core/types/api' import { UserAction, UserRole } from '@api/core/types/user' import { LabelMappingService } from '@api/label-mapping/label-mapping.service' import { PublicTaskSerializer } from '@api/tasks/public/public.serializer' import { SubtaskService } from '@api/tasks/subtasks.service' -import { - dispatchUpdatedWebhookEvent, - getArchivedStatus, - getTaskTimestamps, - queueBodyUpdatedWebhook, -} from '@api/tasks/tasks.helpers' +import { dispatchUpdatedWebhookEvent, getArchivedStatus, getTaskTimestamps } from '@api/tasks/tasks.helpers' import { TasksActivityLogger } from '@api/tasks/tasks.logger' +import { TasksSharedService } from '@/app/api/tasks/tasksShared.service' import { AssigneeType, Prisma, PrismaClient, Source, StateType, Task, TaskTemplate, WorkflowState } from '@prisma/client' import httpStatus from 'http-status' import { z } from 'zod' -import { TemplatesService } from './templates/templates.service' - -export class TasksService extends BaseService { - /** - * Builds filter for "get" service methods. - * If user is an IU, return filter for all tasks associated with this workspace - * If user is a client, return filter for just the tasks assigned to this clientId. - * If user is a client and has a companyId, return filter for just the tasks assigned to this clientId `OR` to this companyId - */ - private buildTaskPermissions(id?: string, includeViewer: boolean = true) { - const user = this.user - - // Default filters - let filters: Prisma.TaskWhereInput = { - id, - workspaceId: user.workspaceId, - } - - if (user.clientId || user.companyId) { - filters = { ...filters, ...this.getClientOrCompanyAssigneeFilter(includeViewer) } - } - - return filters - } +import { TemplatesService } from '@api/tasks/templates/templates.service' +export class TasksService extends TasksSharedService { async getAllTasks(queryFilters: { // Global filters all?: boolean showIncompleteOnly?: boolean - // Public Api filters - fromPublicApi?: boolean + // Column filters showArchived: boolean showUnarchived: boolean @@ -77,8 +46,6 @@ export class TasksService extends BaseService { createdById?: string parentId?: string | null workflowState?: { type: StateType | { not: StateType } } - limit?: number - lastIdCursor?: string // When this id field cursor is provided, we return data AFTER this id }): Promise { // Check if given user role is authorized access to this resource const policyGate = new PoliciesService(this.user) @@ -100,7 +67,7 @@ export class TasksService extends BaseService { isArchived = getArchivedStatus(queryFilters.showArchived, queryFilters.showUnarchived) } - if (queryFilters.showIncompleteOnly && !queryFilters.fromPublicApi) { + if (queryFilters.showIncompleteOnly) { queryFilters.workflowState = { type: { not: StateType.completed }, } @@ -132,26 +99,16 @@ export class TasksService extends BaseService { } const orderBy: Prisma.TaskOrderByWithRelationInput[] = [{ createdAt: 'desc' }] - if (!queryFilters.fromPublicApi) { - // For web, we show dueDate as the primary sort key - orderBy.unshift({ dueDate: { sort: 'asc', nulls: 'last' } }) - } - - const pagination: Prisma.TaskFindManyArgs = { - take: queryFilters.limit, - cursor: queryFilters.lastIdCursor ? { id: queryFilters.lastIdCursor } : undefined, - skip: queryFilters.lastIdCursor ? 1 : undefined, - } + orderBy.unshift({ dueDate: { sort: 'asc', nulls: 'last' } }) const tasks = await this.db.task.findMany({ where, orderBy, - ...pagination, relationLoadStrategy: 'join', include: { workflowState: true }, }) - if (!this.user.internalUserId || queryFilters.fromPublicApi) { + if (!this.user.internalUserId) { return tasks } @@ -163,10 +120,7 @@ export class TasksService extends BaseService { return filteredTasks } - async createTask( - data: CreateTaskRequest, - opts?: { isPublicApi?: boolean; disableSubtaskTemplates?: boolean; manualTimestamp?: Date }, - ) { + async createTask(data: CreateTaskRequest, opts?: { disableSubtaskTemplates?: boolean; manualTimestamp?: Date }) { const policyGate = new PoliciesService(this.user) policyGate.authorize(UserAction.Create, Resource.Tasks) console.info('TasksService#createTask | Creating task with data:', data) @@ -205,16 +159,6 @@ export class TasksService extends BaseService { // NOTE: This block strictly doesn't allow clients to create tasks let createdById = z.string().parse(this.user.internalUserId) - if (data.createdById && opts?.isPublicApi) { - const internalUsers = await this.copilot.getInternalUsers({ limit: MAX_FETCH_ASSIGNEE_COUNT }) // there are 2 of these api running on this same serive. need to think about this later. - const createdBy = internalUsers?.data?.find((iu) => iu.id === data.createdById) - if (!createdBy) { - throw new APIError(httpStatus.BAD_REQUEST, 'The requested user for createdBy was not found') - } - createdById = createdBy.id - console.info('TasksService#createTask | createdById overridden for public API:', createdById) - } - let viewers: Viewers = [] if (data.viewers?.length) { if (!validatedIds.internalUserId) { @@ -233,7 +177,7 @@ export class TasksService extends BaseService { label: label, completedBy, completedByUserType, - source: opts?.isPublicApi ? Source.api : Source.web, + source: Source.web, assigneeId, assigneeType, viewers: viewers, @@ -321,14 +265,13 @@ export class TasksService extends BaseService { return newTask } - async getOneTask(id: string, fromPublicApi?: boolean): Promise { + async getOneTask(id: string): Promise { const policyGate = new PoliciesService(this.user) policyGate.authorize(UserAction.Read, Resource.Tasks) // Build query filters based on role of user. IU can access all tasks related to a workspace // while clients can only view the tasks assigned to them or their company - const filters = this.buildTaskPermissions(id) - const where = fromPublicApi ? { ...filters, deletedAt: { not: undefined } } : filters + const where = this.buildTaskPermissions(id) const task = await this.db.task.findFirst({ where }) if (!task) throw new APIError(httpStatus.NOT_FOUND, 'The requested task was not found') @@ -364,7 +307,7 @@ export class TasksService extends BaseService { } } - async updateOneTask(id: string, data: UpdateTaskRequest, opts?: { isPublicApi: boolean }) { + async updateOneTask(id: string, data: UpdateTaskRequest) { const policyGate = new PoliciesService(this.user) policyGate.authorize(UserAction.Update, Resource.Tasks) @@ -477,8 +420,7 @@ export class TasksService extends BaseService { activityLogger.logTaskUpdated(prevTask), this.setNewLastSubtaskUpdated(updatedTask.parentId), sendTaskUpdateNotifications.trigger({ prevTask, updatedTask, user: this.user }), - dispatchUpdatedWebhookEvent(this.user, prevTask, updatedTask, opts?.isPublicApi || false), - isBodyChanged && opts?.isPublicApi ? queueBodyUpdatedWebhook(this.user, updatedTask) : undefined, + dispatchUpdatedWebhookEvent(this.user, prevTask, updatedTask, false), ]) } @@ -544,232 +486,6 @@ export class TasksService extends BaseService { // await notificationService.deleteInternalUserNotificationForTask(id) } - async getPathOfTask(id: string) { - return ( - await this.db.$queryRaw<{ path: string }[] | null>` - SELECT "path" - FROM "Tasks" - WHERE id::text = ${id} - AND "workspaceId" = ${this.user.workspaceId} - ` - )?.[0]?.path - } - - private getClientOrCompanyAssigneeFilter(includeViewer: boolean = true): Prisma.TaskWhereInput { - const clientId = z.string().uuid().safeParse(this.user.clientId).data - const companyId = z.string().uuid().parse(this.user.companyId) - - const filters = [] - - if (clientId && companyId) { - filters.push( - // Get client tasks for the particular companyId - { clientId, companyId }, - // Get company tasks for the client's companyId - { companyId, clientId: null }, - ) - if (includeViewer) - filters.push( - // Get tasks that includes the client as a viewer - { - viewers: { - hasSome: [{ clientId, companyId }, { companyId }], - }, - }, - ) - } else if (companyId) { - filters.push( - // Get only company tasks for the client's companyId - { clientId: null, companyId }, - ) - if (includeViewer) - filters.push( - // Get tasks that includes the company as a viewer - { - viewers: { - hasSome: [{ companyId }], - }, - }, - ) - } - return filters.length > 0 ? { OR: filters } : {} - } - - private getDisjointTasksFilter = () => { - // For disjoint tasks, show this subtask as a root-level task - // This n-node matcher matches any task tree chain where previous task's assigneeId is not self's - // E.g. A -> B -> C, where A is assigned to user 1, B is assigned to user 2, C is assigned to user 2 - // For user 2, task B should show up as a parent task in the main task board - const disjointTasksFilter: Promise = (async () => { - if (this.user.role === UserRole.IU && !this.user.clientId && !this.user.companyId) { - const currentInternalUser = await this.copilot.getInternalUser(z.string().parse(this.user.internalUserId)) - if (!currentInternalUser.isClientAccessLimited) return {} - - const accesibleCompanyIds = currentInternalUser.companyAccessList || [] - return { - OR: [ - { - parent: { companyId: { notIn: accesibleCompanyIds } }, - }, - { - parentId: null, - }, - ], - } - } - - return { - OR: [ - // Parent is not assigned to client - { - ...this.getClientOrCompanyAssigneeFilter(), // Prevent overwriting of OR statement - parent: { - AND: [ - { - OR: [ - // Disjoint task if parent has no assignee - { clientId: null, companyId: null }, - { - NOT: { - // Do not disjoint task if parent task belongs to the same client / company - OR: [ - // Disjoint task if parent is a client task for a different client under the same company - { clientId: this.user.clientId, companyId: this.user.companyId }, - // Disjoint task if parent is not a company task for the same company that client belongs to - { clientId: null, companyId: this.user.companyId }, - ], - }, - }, - ], - }, - { - NOT: { - viewers: { - hasSome: [ - { clientId: this.user.clientId, companyId: this.user.companyId }, - { companyId: this.user.companyId }, - ], - }, - }, //AND do not disjoint if parent is accesible to the client through client visibility. - }, - ], - }, - }, - // Task is a parent / standalone task - { - ...this.getClientOrCompanyAssigneeFilter(), - parentId: null, - }, - ], - } - })() - - return disjointTasksFilter - } - - private async getParentIdFilter(parentId?: string | null) { - // If `parentId` is present, filter by parentId - if (parentId) { - return z.string().uuid().parse(parentId) - } - if (this.user.companyId) { - // If user is client, flatten subtasks by not filtering by parentId right now - return undefined - } - // If user is IU, no need to flatten subtasks - if (this.user.role === UserRole.IU && !this.user.clientId) { - if (this.user.internalUserId) { - const currentInternalUser = await this.copilot.getInternalUser(this.user.internalUserId) - if (currentInternalUser.isClientAccessLimited) { - return undefined - } - } - return null - } - return undefined - } - - private async addPathToTask(task: Task) { - let path: string = buildLtreeNodeString(task.id) - if (task.parentId) { - const parentPath = await this.getPathOfTask(task.parentId) - if (!parentPath) { - throw new APIError(httpStatus.NOT_FOUND, 'The requested parent task was not found') - } - path = buildLtree(parentPath, task.id) - } - - await this.db.$executeRaw` - UPDATE "Tasks" - SET path = ${buildLtreeNodeString(path)}::ltree - WHERE id::text = ${task.id} - AND "workspaceId" = ${this.user.workspaceId} - ` - } - - private async updateTaskIdOfAttachmentsAfterCreation(htmlString: string, task_id: string) { - const imgTagRegex = /]*src="([^"]+)"[^>]*>/g //expression used to match all img srcs in provided HTML string. - const attachmentTagRegex = /<\s*[a-zA-Z]+\s+[^>]*data-type="attachment"[^>]*src="([^"]+)"[^>]*>/g //expression used to match all attachment srcs in provided HTML string. - let match - const replacements: { originalSrc: string; newUrl: string }[] = [] - - const newFilePaths: { originalSrc: string; newFilePath: string }[] = [] - const copyAttachmentPromises: Promise[] = [] - const matches: { originalSrc: string; filePath: string; fileName: string }[] = [] - - while ((match = imgTagRegex.exec(htmlString)) !== null) { - const originalSrc = match[1] - const filePath = getFilePathFromUrl(originalSrc) - const fileName = filePath?.split('/').pop() - if (filePath && fileName) { - matches.push({ originalSrc, filePath, fileName }) - } - } - - while ((match = attachmentTagRegex.exec(htmlString)) !== null) { - const originalSrc = match[1] - const filePath = getFilePathFromUrl(originalSrc) - const fileName = filePath?.split('/').pop() - if (filePath && fileName) { - matches.push({ originalSrc, filePath, fileName }) - } - } - - for (const { originalSrc, filePath, fileName } of matches) { - const newFilePath = `${this.user.workspaceId}/${task_id}/${fileName}` - const supabaseActions = new SupabaseActions() - copyAttachmentPromises.push(supabaseActions.moveAttachment(filePath, newFilePath)) - newFilePaths.push({ originalSrc, newFilePath }) - } - - await Promise.all(copyAttachmentPromises) - - const signedUrlPromises = newFilePaths.map(async ({ originalSrc, newFilePath }) => { - const newUrl = await getSignedUrl(newFilePath) - if (newUrl) { - replacements.push({ originalSrc, newUrl }) - } - }) - - await Promise.all(signedUrlPromises) - - for (const { originalSrc, newUrl } of replacements) { - htmlString = htmlString.replace(originalSrc, newUrl) - } - const filePaths = newFilePaths.map(({ newFilePath }) => newFilePath) - await this.db.scrapMedia.updateMany({ - where: { - filePath: { - in: filePaths, - }, - }, - data: { - taskId: task_id, - }, - }) - return htmlString - } - async setNewLastActivityLogUpdated(taskId: string) { // This shouldn't crash our app, just in case try { @@ -935,263 +651,4 @@ export class TasksService extends BaseService { const subtaskService = new SubtaskService(this.user) return await subtaskService.getAccessiblePathTasks(parentTasks) } - - private async checkClientAccessForTask(task: Task, internalUserId: string) { - const currentInternalUser = await this.copilot.getInternalUser(internalUserId) - if (!currentInternalUser.isClientAccessLimited) return - - const isLimitedTask = !(await this.filterTasksByClientAccess([task], currentInternalUser)).length - if (isLimitedTask) { - throw new APIError( - httpStatus.UNAUTHORIZED, - "This task's assignee is not included in your list of accessible clients / companies", - ) - } - } - - private async filterTasksByClientAccess(tasks: T[], currentInternalUser: InternalUsers): Promise { - const hasClientOrCompanyTasks = tasks.some((task) => task.companyId) - if (!hasClientOrCompanyTasks) { - return tasks - } - - return tasks.filter((task) => { - // Pass all tasks that are unassigned or are assigned to IU - if ((!task.internalUserId && !task.clientId && !task.companyId) || task.internalUserId) return true - // For remaining client or company tasks, check if companyAccessList includes this companyId - return currentInternalUser.companyAccessList?.includes(task.companyId || '') - }) - } - - async hasMoreTasksAfterCursor( - id: string, - publicFilters: Partial[0]>, - ): Promise { - const nextTask = await this.db.task.findFirst({ - where: { ...publicFilters, workspaceId: this.user.workspaceId }, - cursor: { id }, - skip: 1, - orderBy: { createdAt: 'desc' }, - }) - return !!nextTask - } - - async canCreateSubTask(taskId: string): Promise { - const parentPath = await this.getPathOfTask(taskId) - if (!parentPath) { - throw new APIError(httpStatus.NOT_FOUND, 'The requested parent task was not found') - } - const uuidLength = parentPath.split('.').length - if (!uuidLength) return true - return uuidLength <= maxSubTaskDepth - } - - private async getCompletionInfo(targetWorkflowStateId?: string | null): Promise<{ - completedBy: string | null - completedByUserType: AssigneeType | null - workflowStateStatus: StateType - }> { - if (!targetWorkflowStateId) { - return { completedBy: null, completedByUserType: null, workflowStateStatus: StateType.unstarted } - } - - const role = this.user.role - - const workflowState = await this.db.workflowState.findFirst({ - where: { id: targetWorkflowStateId, workspaceId: this.user.workspaceId }, - select: { type: true }, - }) - - if (!workflowState) { - throw new APIError(httpStatus.NOT_FOUND, 'The requested workflow state was not found') - } - - if (workflowState.type === StateType.completed) { - return { - completedBy: z.string().parse(role === AssigneeType.internalUser ? this.user.internalUserId : this.user.clientId), - completedByUserType: role, - workflowStateStatus: workflowState.type, - } - } - - return { completedBy: null, completedByUserType: null, workflowStateStatus: workflowState.type } - } - - private async validateUserIds( - internalUserId?: string | null, - clientId?: string | null, - companyId?: string | null, - ): Promise<{ - internalUserId: string | null - clientId: string | null - companyId: string | null - }> { - if (internalUserId) { - const internalUsers = (await this.copilot.getInternalUsers({ limit: MAX_FETCH_ASSIGNEE_COUNT })).data - const isValid = internalUsers?.some((user) => user.id === internalUserId) - - if (!isValid) { - throw new APIError(httpStatus.BAD_REQUEST, `Invalid internalUserId`) - } - - return { - internalUserId, - clientId: null, - companyId: null, - } - } - - if (clientId) { - const client = await this.copilot.getClient(clientId) - - const isValidCompany = companyId ? client?.companyIds?.includes(companyId) : false - - if (!client) { - throw new APIError(httpStatus.BAD_REQUEST, `Invalid clientId`) - } - - if (!companyId || !isValidCompany) { - throw new APIError(httpStatus.BAD_REQUEST, `Invalid company for the provided clientId`) - } - - return { - internalUserId: null, - clientId, - companyId, - } - } - - if (companyId) { - const companies = (await this.copilot.getCompanies({ limit: MAX_FETCH_ASSIGNEE_COUNT })).data - const isValid = companies?.some((company) => company.id === companyId) - - if (!isValid) { - throw new APIError(httpStatus.BAD_REQUEST, `Invalid companyId`) - } - - return { - internalUserId: null, - clientId: null, - companyId, - } - } - - return { - internalUserId: null, - clientId: null, - companyId: null, - } - } - - private getAssigneeFromUserIds(userIds: { - internalUserId: string | null - clientId: string | null - companyId: string | null - }): { assigneeId: string | null; assigneeType: AssigneeType | null } { - const { internalUserId, clientId, companyId } = userIds - - if (internalUserId) { - return { - assigneeId: internalUserId, - assigneeType: AssigneeType.internalUser, - } - } - - if (clientId) { - return { - assigneeId: clientId, - assigneeType: AssigneeType.client, - } - } - - if (companyId) { - return { - assigneeId: companyId, - assigneeType: AssigneeType.company, - } - } - return { - assigneeId: null, - assigneeType: null, - } - } - - private async setNewLastSubtaskUpdated(parentId?: z.infer | null) { - if (!parentId) { - return - } - try { - await this.db.task.update({ - where: { id: parentId, workspaceId: this.user.workspaceId }, - data: { - lastSubtaskUpdated: new Date(), - }, - }) - } catch (e) { - console.error('TaskService#setNewLastSubtaskUpdated::', e) - } - } - - private async validateViewers(viewers: Viewers) { - if (!viewers?.length) return [] - const viewer = viewers[0] - try { - if (viewer.clientId) { - const client = await this.copilot.getClient(viewer.clientId) //support looping viewers and filtering from getClients instead of doing getClient if we do support many viewers in the future. - if (!client.companyIds?.includes(viewers[0].companyId)) { - throw new APIError(httpStatus.BAD_REQUEST, 'Invalid companyId for the provided viewer.') - } - } else { - await this.copilot.getCompany(viewer.companyId) - } - } catch (err) { - if (err instanceof APIError) { - throw err - } - throw new APIError(httpStatus.BAD_REQUEST, `Viewer should be a CU.`) - } - - return viewers - } - - private async createSubtasksFromTemplate(data: TaskTemplate, parentTask: Task, manualTimestamp: Date) { - const { workspaceId, title, body, workflowStateId } = data - const previewMode = Boolean(this.user.clientId || this.user.companyId) - const { id: parentId, internalUserId, clientId, companyId, viewers } = parentTask - - try { - const createTaskPayload = CreateTaskRequestSchema.parse({ - title, - body, - workspaceId, - workflowStateId, - parentId, - templateId: undefined, //just to be safe from circular recursion - ...(previewMode && { - internalUserId, - clientId, - companyId, - viewers, - }), //On CRM view, we set assignee and viewers for subtasks same as the parent task. - }) - - await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp: manualTimestamp }) - } catch (e) { - const deleteTask = this.db.task.delete({ where: { id: parentId } }) - const deleteActivityLogs = this.db.activityLog.deleteMany({ where: { taskId: parentId } }) - - await this.db.$transaction(async (tx) => { - this.setTransaction(tx as PrismaClient) - await deleteTask - await deleteActivityLogs - this.unsetTransaction() - }) - - console.error('TasksService#createTask | Rolling back task creation', e) - throw new APIError( - httpStatus.INTERNAL_SERVER_ERROR, - 'Failed to create subtask from template, new task was not created.', - ) - } - } } diff --git a/src/app/api/tasks/tasksShared.service.ts b/src/app/api/tasks/tasksShared.service.ts new file mode 100644 index 000000000..d5ca9abf8 --- /dev/null +++ b/src/app/api/tasks/tasksShared.service.ts @@ -0,0 +1,516 @@ +import { maxSubTaskDepth } from '@/constants/tasks' +import { MAX_FETCH_ASSIGNEE_COUNT } from '@/constants/users' +import { InternalUsers, Uuid } from '@/types/common' +import { CreateTaskRequest, CreateTaskRequestSchema, Viewers } from '@/types/dto/tasks.dto' +import { buildLtree, buildLtreeNodeString } from '@/utils/ltree' +import { getFilePathFromUrl } from '@/utils/signedUrlReplacer' +import { getSignedUrl } from '@/utils/signUrl' +import { SupabaseActions } from '@/utils/SupabaseActions' +import { BaseService } from '@api/core/services/base.service' +import { AssigneeType, Prisma, PrismaClient, StateType, Task, TaskTemplate } from '@prisma/client' +import httpStatus from 'http-status' +import z from 'zod' +import APIError from '@api/core/exceptions/api' +import { UserRole } from '@api/core/types/user' + +//Base class with shared permission logic and methods that both tasks.service.ts and public.service.ts could use +export abstract class TasksSharedService extends BaseService { + protected abstract createTask( + data: CreateTaskRequest, + opts?: { disableSubtaskTemplates?: boolean; manualTimestamp?: Date }, + ): Promise + + /** + * Builds filter for "get" service methods. + * If user is an IU, return filter for all tasks associated with this workspace + * If user is a client, return filter for just the tasks assigned to this clientId. + * If user is a client and has a companyId, return filter for just the tasks assigned to this clientId `OR` to this companyId + */ + protected buildTaskPermissions(id?: string, includeViewer: boolean = true) { + const user = this.user + + // Default filters + let filters: Prisma.TaskWhereInput = { + id, + workspaceId: user.workspaceId, + } + + if (user.clientId || user.companyId) { + filters = { ...filters, ...this.getClientOrCompanyAssigneeFilter(includeViewer) } + } + + return filters + } + + protected getClientOrCompanyAssigneeFilter(includeViewer: boolean = true): Prisma.TaskWhereInput { + const clientId = z.string().uuid().safeParse(this.user.clientId).data + const companyId = z.string().uuid().parse(this.user.companyId) + + const filters = [] + + if (clientId && companyId) { + filters.push( + // Get client tasks for the particular companyId + { clientId, companyId }, + // Get company tasks for the client's companyId + { companyId, clientId: null }, + ) + if (includeViewer) + filters.push( + // Get tasks that includes the client as a viewer + { + viewers: { + hasSome: [{ clientId, companyId }, { companyId }], + }, + }, + ) + } else if (companyId) { + filters.push( + // Get only company tasks for the client's companyId + { clientId: null, companyId }, + ) + if (includeViewer) + filters.push( + // Get tasks that includes the company as a viewer + { + viewers: { + hasSome: [{ companyId }], + }, + }, + ) + } + return filters.length > 0 ? { OR: filters } : {} + } + + protected async getParentIdFilter(parentId?: string | null) { + // If `parentId` is present, filter by parentId + if (parentId) { + return z.string().uuid().parse(parentId) + } + if (this.user.companyId) { + // If user is client, flatten subtasks by not filtering by parentId right now + return undefined + } + // If user is IU, no need to flatten subtasks + if (this.user.role === UserRole.IU && !this.user.clientId) { + if (this.user.internalUserId) { + const currentInternalUser = await this.copilot.getInternalUser(this.user.internalUserId) + if (currentInternalUser.isClientAccessLimited) { + return undefined + } + } + return null + } + return undefined + } + + protected getDisjointTasksFilter = () => { + // For disjoint tasks, show this subtask as a root-level task + // This n-node matcher matches any task tree chain where previous task's assigneeId is not self's + // E.g. A -> B -> C, where A is assigned to user 1, B is assigned to user 2, C is assigned to user 2 + // For user 2, task B should show up as a parent task in the main task board + const disjointTasksFilter: Promise = (async () => { + if (this.user.role === UserRole.IU && !this.user.clientId && !this.user.companyId) { + const currentInternalUser = await this.copilot.getInternalUser(z.string().parse(this.user.internalUserId)) + if (!currentInternalUser.isClientAccessLimited) return {} + + const accesibleCompanyIds = currentInternalUser.companyAccessList || [] + return { + OR: [ + { + parent: { companyId: { notIn: accesibleCompanyIds } }, + }, + { + parentId: null, + }, + ], + } + } + + return { + OR: [ + // Parent is not assigned to client + { + ...this.getClientOrCompanyAssigneeFilter(), // Prevent overwriting of OR statement + parent: { + AND: [ + { + OR: [ + // Disjoint task if parent has no assignee + { clientId: null, companyId: null }, + { + NOT: { + // Do not disjoint task if parent task belongs to the same client / company + OR: [ + // Disjoint task if parent is a client task for a different client under the same company + { clientId: this.user.clientId, companyId: this.user.companyId }, + // Disjoint task if parent is not a company task for the same company that client belongs to + { clientId: null, companyId: this.user.companyId }, + ], + }, + }, + ], + }, + { + NOT: { + viewers: { + hasSome: [ + { clientId: this.user.clientId, companyId: this.user.companyId }, + { companyId: this.user.companyId }, + ], + }, + }, //AND do not disjoint if parent is accesible to the client through client visibility. + }, + ], + }, + }, + // Task is a parent / standalone task + { + ...this.getClientOrCompanyAssigneeFilter(), + parentId: null, + }, + ], + } + })() + + return disjointTasksFilter + } + + protected async checkClientAccessForTask(task: Task, internalUserId: string) { + const currentInternalUser = await this.copilot.getInternalUser(internalUserId) + if (!currentInternalUser.isClientAccessLimited) return + + const isLimitedTask = !(await this.filterTasksByClientAccess([task], currentInternalUser)).length + if (isLimitedTask) { + throw new APIError( + httpStatus.UNAUTHORIZED, + "This task's assignee is not included in your list of accessible clients / companies", + ) + } + } + + protected async filterTasksByClientAccess(tasks: T[], currentInternalUser: InternalUsers): Promise { + const hasClientOrCompanyTasks = tasks.some((task) => task.companyId) + if (!hasClientOrCompanyTasks) { + return tasks + } + + return tasks.filter((task) => { + // Pass all tasks that are unassigned or are assigned to IU + if ((!task.internalUserId && !task.clientId && !task.companyId) || task.internalUserId) return true + // For remaining client or company tasks, check if companyAccessList includes this companyId + return currentInternalUser.companyAccessList?.includes(task.companyId || '') + }) + } + + protected async validateUserIds( + internalUserId?: string | null, + clientId?: string | null, + companyId?: string | null, + ): Promise<{ + internalUserId: string | null + clientId: string | null + companyId: string | null + }> { + if (internalUserId) { + const internalUsers = (await this.copilot.getInternalUsers({ limit: MAX_FETCH_ASSIGNEE_COUNT })).data + const isValid = internalUsers?.some((user) => user.id === internalUserId) + + if (!isValid) { + throw new APIError(httpStatus.BAD_REQUEST, `Invalid internalUserId`) + } + + return { + internalUserId, + clientId: null, + companyId: null, + } + } + + if (clientId) { + const client = await this.copilot.getClient(clientId) + + const isValidCompany = companyId ? client?.companyIds?.includes(companyId) : false + + if (!client) { + throw new APIError(httpStatus.BAD_REQUEST, `Invalid clientId`) + } + + if (!companyId || !isValidCompany) { + throw new APIError(httpStatus.BAD_REQUEST, `Invalid company for the provided clientId`) + } + + return { + internalUserId: null, + clientId, + companyId, + } + } + + if (companyId) { + const companies = (await this.copilot.getCompanies({ limit: MAX_FETCH_ASSIGNEE_COUNT })).data + const isValid = companies?.some((company) => company.id === companyId) + + if (!isValid) { + throw new APIError(httpStatus.BAD_REQUEST, `Invalid companyId`) + } + + return { + internalUserId: null, + clientId: null, + companyId, + } + } + + return { + internalUserId: null, + clientId: null, + companyId: null, + } + } + + protected getAssigneeFromUserIds(userIds: { + internalUserId: string | null + clientId: string | null + companyId: string | null + }): { assigneeId: string | null; assigneeType: AssigneeType | null } { + const { internalUserId, clientId, companyId } = userIds + + if (internalUserId) { + return { + assigneeId: internalUserId, + assigneeType: AssigneeType.internalUser, + } + } + + if (clientId) { + return { + assigneeId: clientId, + assigneeType: AssigneeType.client, + } + } + + if (companyId) { + return { + assigneeId: companyId, + assigneeType: AssigneeType.company, + } + } + return { + assigneeId: null, + assigneeType: null, + } + } + + protected async canCreateSubTask(taskId: string): Promise { + const parentPath = await this.getPathOfTask(taskId) + if (!parentPath) { + throw new APIError(httpStatus.NOT_FOUND, 'The requested parent task was not found') + } + const uuidLength = parentPath.split('.').length + if (!uuidLength) return true + return uuidLength <= maxSubTaskDepth + } + + private async getPathOfTask(id: string) { + return ( + await this.db.$queryRaw<{ path: string }[] | null>` + SELECT "path" + FROM "Tasks" + WHERE id::text = ${id} + AND "workspaceId" = ${this.user.workspaceId} + ` + )?.[0]?.path + } + + protected async getCompletionInfo(targetWorkflowStateId?: string | null): Promise<{ + completedBy: string | null + completedByUserType: AssigneeType | null + workflowStateStatus: StateType + }> { + if (!targetWorkflowStateId) { + return { completedBy: null, completedByUserType: null, workflowStateStatus: StateType.unstarted } + } + + const role = this.user.role + + const workflowState = await this.db.workflowState.findFirst({ + where: { id: targetWorkflowStateId, workspaceId: this.user.workspaceId }, + select: { type: true }, + }) + + if (!workflowState) { + throw new APIError(httpStatus.NOT_FOUND, 'The requested workflow state was not found') + } + + if (workflowState.type === StateType.completed) { + return { + completedBy: z.string().parse(role === AssigneeType.internalUser ? this.user.internalUserId : this.user.clientId), + completedByUserType: role, + workflowStateStatus: workflowState.type, + } + } + + return { completedBy: null, completedByUserType: null, workflowStateStatus: workflowState.type } + } + + protected async validateViewers(viewers: Viewers) { + if (!viewers?.length) return [] + const viewer = viewers[0] + try { + if (viewer.clientId) { + const client = await this.copilot.getClient(viewer.clientId) //support looping viewers and filtering from getClients instead of doing getClient if we do support many viewers in the future. + if (!client.companyIds?.includes(viewers[0].companyId)) { + throw new APIError(httpStatus.BAD_REQUEST, 'Invalid companyId for the provided viewer.') + } + } else { + await this.copilot.getCompany(viewer.companyId) + } + } catch (err) { + if (err instanceof APIError) { + throw err + } + throw new APIError(httpStatus.BAD_REQUEST, `Viewer should be a CU.`) + } + + return viewers + } + + protected async updateTaskIdOfAttachmentsAfterCreation(htmlString: string, task_id: string) { + const imgTagRegex = /]*src="([^"]+)"[^>]*>/g //expression used to match all img srcs in provided HTML string. + const attachmentTagRegex = /<\s*[a-zA-Z]+\s+[^>]*data-type="attachment"[^>]*src="([^"]+)"[^>]*>/g //expression used to match all attachment srcs in provided HTML string. + let match + const replacements: { originalSrc: string; newUrl: string }[] = [] + + const newFilePaths: { originalSrc: string; newFilePath: string }[] = [] + const copyAttachmentPromises: Promise[] = [] + const matches: { originalSrc: string; filePath: string; fileName: string }[] = [] + + while ((match = imgTagRegex.exec(htmlString)) !== null) { + const originalSrc = match[1] + const filePath = getFilePathFromUrl(originalSrc) + const fileName = filePath?.split('/').pop() + if (filePath && fileName) { + matches.push({ originalSrc, filePath, fileName }) + } + } + + while ((match = attachmentTagRegex.exec(htmlString)) !== null) { + const originalSrc = match[1] + const filePath = getFilePathFromUrl(originalSrc) + const fileName = filePath?.split('/').pop() + if (filePath && fileName) { + matches.push({ originalSrc, filePath, fileName }) + } + } + + for (const { originalSrc, filePath, fileName } of matches) { + const newFilePath = `${this.user.workspaceId}/${task_id}/${fileName}` + const supabaseActions = new SupabaseActions() + copyAttachmentPromises.push(supabaseActions.moveAttachment(filePath, newFilePath)) + newFilePaths.push({ originalSrc, newFilePath }) + } + + await Promise.all(copyAttachmentPromises) + + const signedUrlPromises = newFilePaths.map(async ({ originalSrc, newFilePath }) => { + const newUrl = await getSignedUrl(newFilePath) + if (newUrl) { + replacements.push({ originalSrc, newUrl }) + } + }) + + await Promise.all(signedUrlPromises) + + for (const { originalSrc, newUrl } of replacements) { + htmlString = htmlString.replace(originalSrc, newUrl) + } + const filePaths = newFilePaths.map(({ newFilePath }) => newFilePath) + await this.db.scrapMedia.updateMany({ + where: { + filePath: { + in: filePaths, + }, + }, + data: { + taskId: task_id, + }, + }) + return htmlString + } + + protected async addPathToTask(task: Task) { + let path: string = buildLtreeNodeString(task.id) + if (task.parentId) { + const parentPath = await this.getPathOfTask(task.parentId) + if (!parentPath) { + throw new APIError(httpStatus.NOT_FOUND, 'The requested parent task was not found') + } + path = buildLtree(parentPath, task.id) + } + + await this.db.$executeRaw` + UPDATE "Tasks" + SET path = ${buildLtreeNodeString(path)}::ltree + WHERE id::text = ${task.id} + AND "workspaceId" = ${this.user.workspaceId} + ` + } + + protected async setNewLastSubtaskUpdated(parentId?: z.infer | null) { + if (!parentId) { + return + } + try { + await this.db.task.update({ + where: { id: parentId, workspaceId: this.user.workspaceId }, + data: { + lastSubtaskUpdated: new Date(), + }, + }) + } catch (e) { + console.error('TaskService#setNewLastSubtaskUpdated::', e) + } + } + + protected async createSubtasksFromTemplate(data: TaskTemplate, parentTask: Task, manualTimestamp: Date) { + const { workspaceId, title, body, workflowStateId } = data + const previewMode = Boolean(this.user.clientId || this.user.companyId) + const { id: parentId, internalUserId, clientId, companyId, viewers } = parentTask + + try { + const createTaskPayload = CreateTaskRequestSchema.parse({ + title, + body, + workspaceId, + workflowStateId, + parentId, + templateId: undefined, //just to be safe from circular recursion + ...(previewMode && { + internalUserId, + clientId, + companyId, + viewers, + }), //On CRM view, we set assignee and viewers for subtasks same as the parent task. + }) + + await this.createTask(createTaskPayload, { disableSubtaskTemplates: true, manualTimestamp: manualTimestamp }) + } catch (e) { + const deleteTask = this.db.task.delete({ where: { id: parentId } }) + const deleteActivityLogs = this.db.activityLog.deleteMany({ where: { taskId: parentId } }) + + await this.db.$transaction(async (tx) => { + this.setTransaction(tx as PrismaClient) + await deleteTask + await deleteActivityLogs + this.unsetTransaction() + }) + + console.error('TasksService#createTask | Rolling back task creation', e) + throw new APIError( + httpStatus.INTERNAL_SERVER_ERROR, + 'Failed to create subtask from template, new task was not created.', + ) + } + } +} diff --git a/src/app/api/tasks/templates/public/public.controller.ts b/src/app/api/tasks/templates/public/public.controller.ts index 72e6e10d9..2d40b3213 100644 --- a/src/app/api/tasks/templates/public/public.controller.ts +++ b/src/app/api/tasks/templates/public/public.controller.ts @@ -22,7 +22,8 @@ export const getTaskTemplatesPublic = async (req: NextRequest) => { return NextResponse.json({ data: PublicTemplateSerializer.serialize(templates), nextToken: base64NextToken }) } -export const getTaskTemplatePublic = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getTaskTemplatePublic = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const templatesService = new TemplatesService(user) const template = await templatesService.getOneTemplate(id) diff --git a/src/app/api/tasks/templates/public/public.dto.ts b/src/app/api/tasks/templates/public/public.dto.ts index 406b4a14f..a80a79ab0 100644 --- a/src/app/api/tasks/templates/public/public.dto.ts +++ b/src/app/api/tasks/templates/public/public.dto.ts @@ -10,13 +10,23 @@ export interface TemplateResponsePublicType { subTaskTemplates: TemplateResponsePublicType[] } //forward declaring the interface -export const TemplateResponsePublicSchema: z.ZodType = z.object({ +export const SubTemplateResponsePublicSchema = z.object({ id: z.string().uuid(), object: z.literal('taskTemplate'), name: z.string(), description: z.string().nullable(), createdDate: RFC3339DateSchema, - subTaskTemplates: z.array(z.lazy(() => TemplateResponsePublicSchema)), }) +export const TemplateResponsePublicSchema = z.object({ + id: z.string().uuid(), + object: z.literal('taskTemplate'), + name: z.string(), + description: z.string().nullable(), + createdDate: RFC3339DateSchema, + subTaskTemplates: z.array(SubTemplateResponsePublicSchema), +}) + +export type SubTemplateResponsePublic = z.infer + export type TemplateResponsePublic = z.infer diff --git a/src/app/api/tasks/templates/public/public.serializer.ts b/src/app/api/tasks/templates/public/public.serializer.ts index a59559152..e23e1690f 100644 --- a/src/app/api/tasks/templates/public/public.serializer.ts +++ b/src/app/api/tasks/templates/public/public.serializer.ts @@ -1,4 +1,9 @@ -import { TemplateResponsePublic, TemplateResponsePublicSchema } from '@api/tasks/templates/public/public.dto' +import { + SubTemplateResponsePublic, + SubTemplateResponsePublicSchema, + TemplateResponsePublic, + TemplateResponsePublicSchema, +} from '@api/tasks/templates/public/public.dto' import { toRFC3339 } from '@/utils/dateHelper' import { TaskTemplate } from '@prisma/client' import { z } from 'zod' @@ -7,29 +12,41 @@ type TaskTemplateWithSubtasks = TaskTemplate & { subTaskTemplates?: TaskTemplateWithSubtasks[] } export class PublicTemplateSerializer { + private static MAX_DEPTH = 2 + static serialize( template: TaskTemplateWithSubtasks | TaskTemplateWithSubtasks[], - ): TemplateResponsePublic | TemplateResponsePublic[] { + isSubTaskTemplate: boolean = false, + depth: number = 0, + ): TemplateResponsePublic | TemplateResponsePublic[] | SubTemplateResponsePublic | SubTemplateResponsePublic[] { + if (depth > this.MAX_DEPTH) { + throw new Error(`Max recursion depth of ${this.MAX_DEPTH} exceeded for sub-templates.`) + } + const templateSchema = isSubTaskTemplate ? SubTemplateResponsePublicSchema : TemplateResponsePublicSchema if (Array.isArray(template)) { - return z.array(TemplateResponsePublicSchema).parse( + return z.array(templateSchema).parse( template.map((template) => ({ id: template.id, object: 'taskTemplate', name: template.title, description: template.body, createdDate: toRFC3339(template.createdAt), - subTaskTemplates: template.subTaskTemplates?.map((sub) => this.serialize(sub)) ?? [], + ...(!isSubTaskTemplate && { + subTaskTemplates: this.serialize(template.subTaskTemplates ?? [], true, depth + 1), + }), })), ) } - return TemplateResponsePublicSchema.parse({ + return templateSchema.parse({ id: template.id, object: 'taskTemplate', name: template.title, description: template.body, createdDate: toRFC3339(template.createdAt), - subTaskTemplates: template.subTaskTemplates?.map((sub) => this.serialize(sub)) ?? [], + ...(!isSubTaskTemplate && { + subTaskTemplates: this.serialize(template.subTaskTemplates ?? [], true, depth + 1), + }), }) } } diff --git a/src/app/api/tasks/templates/templates.controller.ts b/src/app/api/tasks/templates/templates.controller.ts index f35a3fc93..64efe142e 100644 --- a/src/app/api/tasks/templates/templates.controller.ts +++ b/src/app/api/tasks/templates/templates.controller.ts @@ -24,7 +24,8 @@ export const createTaskTemplate = async (req: NextRequest) => { return NextResponse.json({ data }) } -export const updateTaskTemplate = async (req: NextRequest, { params: { id } }: IdParams) => { +export const updateTaskTemplate = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const payload = UpdateTemplateRequestSchema.parse(await req.json()) @@ -34,7 +35,8 @@ export const updateTaskTemplate = async (req: NextRequest, { params: { id } }: I return NextResponse.json({ data }) } -export const createSubTaskTemplate = async (req: NextRequest, { params: { id } }: IdParams) => { +export const createSubTaskTemplate = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const payload = CreateTemplateRequestSchema.parse(await req.json()) const templatesService = new TemplatesService(user) @@ -42,7 +44,8 @@ export const createSubTaskTemplate = async (req: NextRequest, { params: { id } } return NextResponse.json({ data }) } -export const deleteTaskTemplate = async (req: NextRequest, { params: { id } }: IdParams) => { +export const deleteTaskTemplate = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const templatesService = new TemplatesService(user) @@ -51,7 +54,8 @@ export const deleteTaskTemplate = async (req: NextRequest, { params: { id } }: I return new NextResponse(null, { status: httpStatus.NO_CONTENT }) } -export const applyTemplate = async (req: NextRequest, { params: { id } }: IdParams) => { +export const applyTemplate = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const templatesService = new TemplatesService(user) const data = await templatesService.getAppliedTemplateDescription(id) @@ -59,7 +63,8 @@ export const applyTemplate = async (req: NextRequest, { params: { id } }: IdPara return NextResponse.json({ data }) } -export const getOneTemplate = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getOneTemplate = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const templatesService = new TemplatesService(user) const data = await templatesService.getOneTemplate(id) @@ -67,7 +72,8 @@ export const getOneTemplate = async (req: NextRequest, { params: { id } }: IdPar return NextResponse.json({ data }) } -export const getSubtemplates = async (req: NextRequest, { params: { id } }: IdParams) => { +export const getSubtemplates = async (req: NextRequest, { params }: IdParams) => { + const { id } = await params const user = await authenticate(req) const templatesService = new TemplatesService(user) const data = await templatesService.getSubtemplates(id) diff --git a/src/app/client/page.tsx b/src/app/client/page.tsx index 0a5318bde..51ba540a7 100644 --- a/src/app/client/page.tsx +++ b/src/app/client/page.tsx @@ -54,7 +54,8 @@ async function getWorkspace(token: string): Promise { return await copilot.getWorkspace() } -export default async function ClientPage({ searchParams }: { searchParams: { token: string } & UrlActionParamsType }) { +export default async function ClientPage(props: { searchParams: Promise<{ token: string } & UrlActionParamsType> }) { + const searchParams = await props.searchParams const token = searchParams.token if (!z.string().safeParse(token).success) { return diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx index c5c2e6155..b3a1090aa 100644 --- a/src/app/detail/[task_id]/[user_type]/page.tsx +++ b/src/app/detail/[task_id]/[user_type]/page.tsx @@ -31,7 +31,6 @@ import { HeaderBreadcrumbs } from '@/components/layouts/HeaderBreadcrumbs' import { SilentError } from '@/components/templates/SilentError' import { apiUrl } from '@/config' import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin' -import CustomScrollBar from '@/hoc/CustomScrollBar' import { RealTime } from '@/hoc/RealTime' import { RealTimeTemplates } from '@/hoc/RealtimeTemplates' import { WorkspaceResponse } from '@/types/common' @@ -78,13 +77,12 @@ async function getTaskPath(token: string, taskId: string): Promise + searchParams: Promise<{ token: string; isRedirect?: string; fromNotificationCenter?: string }> }) { + const searchParams = await props.searchParams + const params = await props.params const { token } = searchParams const { task_id, user_type } = params @@ -144,7 +142,7 @@ export default async function TaskDetailPage({ - + {isPreviewMode ? ( @@ -171,55 +169,53 @@ export default async function TaskDetailPage({ )} - - - - - { - 'use server' - await updateTaskDetail({ token, taskId: task_id, payload: { body: detail } }) - }} - updateTaskTitle={async (title) => { - 'use server' - title.trim() != '' && (await updateTaskDetail({ token, taskId: task_id, payload: { title } })) - }} - deleteTask={async () => { - 'use server' - await deleteTask(token, task_id) - }} - postAttachment={async (postAttachmentPayload) => { - 'use server' - await postAttachment(token, postAttachmentPayload) - }} - deleteAttachment={async (id: string) => { - 'use server' - await deleteAttachment(token, id) - }} - userType={params.user_type} - token={token} - /> - - {subTaskStatus.canCreateSubtask && ( - - )} + + + + { + 'use server' + await updateTaskDetail({ token, taskId: task_id, payload: { body: detail } }) + }} + updateTaskTitle={async (title) => { + 'use server' + title.trim() != '' && (await updateTaskDetail({ token, taskId: task_id, payload: { title } })) + }} + deleteTask={async () => { + 'use server' + await deleteTask(token, task_id) + }} + postAttachment={async (postAttachmentPayload) => { + 'use server' + await postAttachment(token, postAttachmentPayload) + }} + deleteAttachment={async (id: string) => { + 'use server' + await deleteAttachment(token, id) + }} + userType={params.user_type} + token={token} + /> + + {subTaskStatus.canCreateSubtask && ( + + )} - - - + + (null) + const [taskViewerValue, setTaskViewerValue] = useState( + !!previewMode + ? (getSelectorAssigneeFromFilterOptions( + assignee, + { internalUserId: null, ...previewClientCompany }, // if preview mode, default select the respective client/company as viewer + ) ?? null) + : null, + ) const applyTemplate = useCallback( (id: string, templateTitle: string) => { @@ -224,6 +231,29 @@ export const NewTaskCard = ({ handleClose() } + const handleAssigneeChange = (inputValue: InputValue[]) => { + if (inputValue.length === 0 || inputValue[0].object !== UserRole.IU) { + setTaskViewerValue(null) + handleFieldChange('viewers', []) + } + if (!!previewMode && inputValue.length && inputValue[0].object === UserRole.IU && previewClientCompany.companyId) { + if (!taskViewerValue) + setTaskViewerValue( + getSelectorAssigneeFromFilterOptions( + assignee, + { internalUserId: null, ...previewClientCompany }, // if preview mode, default select the respective client/company as viewer + ) ?? null, + ) + handleFieldChange('viewers', [ + { clientId: previewClientCompany.clientId || undefined, companyId: previewClientCompany.companyId }, + ]) + } + const newUserIds = getSelectedUserIds(inputValue) + const selectedAssignee = getSelectorAssignee(assignee, inputValue) + setAssigneeValue(selectedAssignee || null) + handleFieldChange('userIds', newUserIds) + } + return ( @@ -360,23 +391,11 @@ export const NewTaskCard = ({ dateValue={subTaskFields.dueDate ?? undefined} /> { - // remove task viewers if assignee is cleared or changed to client or company - if (inputValue.length === 0 || inputValue[0].object !== UserRole.IU) { - setTaskViewerValue(null) - handleFieldChange('viewers', []) - } - const newUserIds = getSelectedUserIds(inputValue) - const selectedAssignee = getSelectorAssignee(assignee, inputValue) - setAssigneeValue(selectedAssignee || null) - handleFieldChange('userIds', newUserIds) - }} + onChange={handleAssigneeChange} initialValue={assigneeValue || undefined} buttonContent={ { borderLeft: (theme) => `1px solid ${theme.color.borders.border2}`, height: '100vh', display: showSidebar ? 'block' : 'none', - width: isMobile && showSidebar ? '100vw' : '320px', + width: isMobile && showSidebar ? '100vw' : '305px', }} > @@ -701,6 +701,14 @@ export const SidebarSkeleton = () => { + + + Assignee + + + + + ) diff --git a/src/app/detail/ui/TaskCardList.tsx b/src/app/detail/ui/TaskCardList.tsx index 68882abd1..b89037623 100644 --- a/src/app/detail/ui/TaskCardList.tsx +++ b/src/app/detail/ui/TaskCardList.tsx @@ -88,6 +88,10 @@ export const TaskCardList = ({ const subtaskCount = useSubtaskCount(task.id) + const [assigneeValue, setAssigneeValue] = useState | undefined>(() => { + return assigneeCache[task.id] + }) //Omitting type for NoAssignee + useEffect(() => { if (!task) return @@ -165,10 +169,6 @@ export const TaskCardList = ({ return match ?? NoAssignee } - const [assigneeValue, setAssigneeValue] = useState | undefined>(() => { - return assigneeCache[task.id] - }) //Omitting type for NoAssignee - return ( deleteEditorAttachmentsHandler(url, token ?? '', task_id, null)} - attachmentLayout={AttachmentLayout} + attachmentLayout={(props) => } addAttachmentButton maxUploadLimit={MAX_UPLOAD_LIMIT} /> diff --git a/src/app/manage-templates/[template_id]/page.tsx b/src/app/manage-templates/[template_id]/page.tsx index 756500abd..e5426a3e7 100644 --- a/src/app/manage-templates/[template_id]/page.tsx +++ b/src/app/manage-templates/[template_id]/page.tsx @@ -28,13 +28,12 @@ async function getTemplate(id: string, token: string): Promise { return templates.data } -export default async function TaskDetailPage({ - params, - searchParams, -}: { - params: { template_id: string } - searchParams: { token: string } +export default async function TaskDetailPage(props: { + params: Promise<{ template_id: string }> + searchParams: Promise<{ token: string }> }) { + const searchParams = await props.searchParams + const params = await props.params const { token } = searchParams const { template_id } = params diff --git a/src/app/manage-templates/page.tsx b/src/app/manage-templates/page.tsx index 65ac7ef67..45413f8ec 100644 --- a/src/app/manage-templates/page.tsx +++ b/src/app/manage-templates/page.tsx @@ -58,12 +58,13 @@ async function getWorkspace(token: string): Promise { } interface ManageTemplatesPageProps { - searchParams: { + searchParams: Promise<{ token: string - } + }> } -export default async function ManageTemplatesPage({ searchParams }: ManageTemplatesPageProps) { +export default async function ManageTemplatesPage(props: ManageTemplatesPageProps) { + const searchParams = await props.searchParams const { token } = searchParams const [workflowStates, assignee, templates, tokenPayload, workspace] = await Promise.all([ getAllWorkflowStates(token), diff --git a/src/app/manage-templates/ui/TemplateDetails.tsx b/src/app/manage-templates/ui/TemplateDetails.tsx index bd4487931..aa3fe5103 100644 --- a/src/app/manage-templates/ui/TemplateDetails.tsx +++ b/src/app/manage-templates/ui/TemplateDetails.tsx @@ -164,7 +164,7 @@ export default function TemplateDetails({ uploadFn={uploadFn} handleImageDoubleClick={handleImagePreview} deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', template_id, null)} - attachmentLayout={AttachmentLayout} + attachmentLayout={(props) => } addAttachmentButton maxUploadLimit={MAX_UPLOAD_LIMIT} /> diff --git a/src/app/manage-templates/ui/TemplateForm.tsx b/src/app/manage-templates/ui/TemplateForm.tsx index eb465fa5b..ac902a706 100644 --- a/src/app/manage-templates/ui/TemplateForm.tsx +++ b/src/app/manage-templates/ui/TemplateForm.tsx @@ -166,7 +166,7 @@ const NewTemplateFormInputs = () => { targetMethod == TargetMethod.POST ? null : targetTemplateId, ) } - attachmentLayout={AttachmentLayout} + attachmentLayout={(props) => } maxUploadLimit={MAX_UPLOAD_LIMIT} parentContainerStyle={{ gap: '0px', minHeight: '60px' }} /> diff --git a/src/app/notification-center/page.tsx b/src/app/notification-center/page.tsx index 85d2548be..e39b77851 100644 --- a/src/app/notification-center/page.tsx +++ b/src/app/notification-center/page.tsx @@ -14,7 +14,8 @@ async function getNotificationDetail(token: string) { return await copilot.getIUNotification(z.string().parse(tokenPayload.notificationId), tokenPayload.workspaceId) // notification "id" is expected in tokenPayload } -export default async function NotificationCenter({ searchParams }: { searchParams: { token: string } }) { +export default async function NotificationCenter(props: { searchParams: Promise<{ token: string }> }) { + const searchParams = await props.searchParams const token = searchParams.token if (!z.string().safeParse(token).success) { return diff --git a/src/app/ui/NewTaskForm.tsx b/src/app/ui/NewTaskForm.tsx index 5606cd25d..cf768f35e 100644 --- a/src/app/ui/NewTaskForm.tsx +++ b/src/app/ui/NewTaskForm.tsx @@ -120,6 +120,59 @@ export const NewTaskForm = ({ handleCreate, handleClose }: NewTaskFormProps) => : null, ) + // this function handles the action param passed in the url and fill the values in the form + const handleUrlActionParam = useCallback(async () => { + if (urlActionParams.pf && token) { + const payload = JSON.parse(decodeURIComponent(urlActionParams.pf)) + + if (!payload.companyId && payload.clientId) { + const assigneeVal = assignee.find((val) => val.id === payload.clientId) + + if (!assigneeVal) { + setErrorMessage('Assignee not found') + delete payload.clientId + } else { + if (Array.isArray(assigneeVal.companyIds) && assigneeVal.companyIds.length === 1) { + payload.companyId = assigneeVal.companyIds[0] + } else if ( + assigneeVal.companyId && + (!assigneeVal.companyIds || (Array.isArray(assigneeVal.companyIds) && !assigneeVal.companyIds.length)) + ) { + payload.companyId = assigneeVal.companyId + } else if (Array.isArray(assigneeVal?.companyIds)) { + // If client has multiple companies, set error + delete payload.clientId + setErrorMessage('companyId must be provided for clients with more than one company') + } else { + delete payload.clientId + setErrorMessage('companyId must be provided when clientId is provided') + } + } + } + + // respect the filter Ids first. This is needed for CRM deep link for respective clients + const assigneeFilter = { + [UserIds.INTERNAL_USER_ID]: payload?.internalUserId || null, + [UserIds.CLIENT_ID]: payload?.clientId || null, + [UserIds.COMPANY_ID]: payload?.companyId || null, + } + + const taskPayload = { + title: payload?.name || '', + description: marked(payload?.description?.replaceAll('\n', '
') || '', { async: false }), + workflowStateId: workflowStates.find((state) => state.key === payload?.status)?.id || '', + dueDate: payload?.dueDate || null, + templateId: payload?.templateId || null, + userIds: assigneeFilter, + parentId: payload?.parentTaskId || null, + } + + setAssigneeValue(getSelectorAssigneeFromFilterOptions(assignee, assigneeFilter) || null) + setActionParamPayload(payload) + store.dispatch(setAllCreateTaskFields(taskPayload)) + } + }, [urlActionParams, assignee]) + useEffect(() => { if (!assignee.length) return if ( @@ -183,59 +236,6 @@ export const NewTaskForm = ({ handleCreate, handleClose }: NewTaskFormProps) => } }, [handleClose]) - // this function handles the action param passed in the url and fill the values in the form - const handleUrlActionParam = useCallback(async () => { - if (urlActionParams.pf && token) { - const payload = JSON.parse(decodeURIComponent(urlActionParams.pf)) - - if (!payload.companyId && payload.clientId) { - const assigneeVal = assignee.find((val) => val.id === payload.clientId) - - if (!assigneeVal) { - setErrorMessage('Assignee not found') - delete payload.clientId - } else { - if (Array.isArray(assigneeVal.companyIds) && assigneeVal.companyIds.length === 1) { - payload.companyId = assigneeVal.companyIds[0] - } else if ( - assigneeVal.companyId && - (!assigneeVal.companyIds || (Array.isArray(assigneeVal.companyIds) && !assigneeVal.companyIds.length)) - ) { - payload.companyId = assigneeVal.companyId - } else if (Array.isArray(assigneeVal?.companyIds)) { - // If client has multiple companies, set error - delete payload.clientId - setErrorMessage('companyId must be provided for clients with more than one company') - } else { - delete payload.clientId - setErrorMessage('companyId must be provided when clientId is provided') - } - } - } - - // respect the filter Ids first. This is needed for CRM deep link for respective clients - const assigneeFilter = { - [UserIds.INTERNAL_USER_ID]: payload?.internalUserId || null, - [UserIds.CLIENT_ID]: payload?.clientId || null, - [UserIds.COMPANY_ID]: payload?.companyId || null, - } - - const taskPayload = { - title: payload?.name || '', - description: marked(payload?.description?.replaceAll('\n', '
') || '', { async: false }), - workflowStateId: workflowStates.find((state) => state.key === payload?.status)?.id || '', - dueDate: payload?.dueDate || null, - templateId: payload?.templateId || null, - userIds: assigneeFilter, - parentId: payload?.parentTaskId || null, - } - - setAssigneeValue(getSelectorAssigneeFromFilterOptions(assignee, assigneeFilter) || null) - setActionParamPayload(payload) - store.dispatch(setAllCreateTaskFields(taskPayload)) - } - }, [urlActionParams, assignee]) - const handleAssigneeChange = (inputValue: InputValue[]) => { // remove task viewers if assignee is cleared or changed to client or company if (inputValue.length === 0 || inputValue[0].object !== UserRole.IU) { @@ -288,7 +288,7 @@ export const NewTaskForm = ({ handleCreate, handleClose }: NewTaskFormProps) => handleClose()} /> @@ -618,7 +619,7 @@ const NewTaskFormInputs = ({ isEditorReadonly }: NewTaskFormInputsProps) => { uploadFn={uploadFn} readonly={isEditorReadonly} deleteEditorAttachments={(url) => deleteEditorAttachmentsHandler(url, token ?? '', null, null)} - attachmentLayout={AttachmentLayout} + attachmentLayout={(props) => } maxUploadLimit={MAX_UPLOAD_LIMIT} parentContainerStyle={{ gap: '0px', minHeight: '60px' }} /> diff --git a/src/app/ui/TaskBoard.tsx b/src/app/ui/TaskBoard.tsx index 6c5334cec..d8c90d9b3 100644 --- a/src/app/ui/TaskBoard.tsx +++ b/src/app/ui/TaskBoard.tsx @@ -4,11 +4,10 @@ import { updateTask } from '@/app/(home)/actions' import { TaskDataFetcher } from '@/app/_fetchers/TaskDataFetcher' import { clientUpdateTask } from '@/app/detail/[task_id]/[user_type]/actions' import { TaskBoardAppBridge } from '@/app/ui/TaskBoardAppBridge' -import { TasksColumnVirtualizer, TasksRowVirtualizer } from '@/app/ui/VirtualizedTasksLists' +import { TasksRowVirtualizer, TasksListVirtualizer } from '@/app/ui/VirtualizedTasksLists' import { CustomDragLayer } from '@/components/CustomDragLayer' import { CardDragLayer } from '@/components/cards/CardDragLayer' import { TaskColumn } from '@/components/cards/TaskColumn' -import { TaskRow } from '@/components/cards/TaskRow' import DashboardEmptyState from '@/components/layouts/EmptyState/DashboardEmptyState' import { FilterBar } from '@/components/layouts/FilterBar' import { SecondaryFilterBar } from '@/components/layouts/SecondaryFilterBar' @@ -23,7 +22,7 @@ import { sortTaskByDescendingOrder } from '@/utils/sortByDescending' import { prioritizeStartedStates } from '@/utils/workflowStates' import { UserRole } from '@api/core/types/user' import { Box, Stack } from '@mui/material' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useSelector } from 'react-redux' import { z } from 'zod' @@ -48,6 +47,7 @@ export const TaskBoard = ({ mode, workspace, token }: TaskBoardProps) => { showArchived, showUnarchived, } = useSelector(selectTaskBoard) + const boardRef = useRef(null) const onDropItem = useCallback( (payload: { taskId: string; targetWorkflowStateId: string }) => { @@ -188,36 +188,17 @@ export const TaskBoard = ({ mode, workspace, token }: TaskBoardProps) => { height: `calc(100vh - 130px)`, width: '99.92%', margin: '0 auto', - overflowY: 'auto', }} > - {prioritizeStartedStates(workflowStates).map((list, index) => ( - - - (filterTaskWithWorkflowStateId(list.id))} - list={list} - mode={mode} - token={token} - subtasksByTaskId={subtasksByTaskId} - /> - - - ))} + )} diff --git a/src/app/ui/VirtualizedTasksLists.tsx b/src/app/ui/VirtualizedTasksLists.tsx index 0eee67ede..3f1f31ebf 100644 --- a/src/app/ui/VirtualizedTasksLists.tsx +++ b/src/app/ui/VirtualizedTasksLists.tsx @@ -13,9 +13,14 @@ import { checkIfTaskViewer } from '@/utils/taskViewer' import { UserRole } from '@api/core/types/user' import { Box } from '@mui/material' import { useVirtualizer } from '@tanstack/react-virtual' -import { useCallback, useRef } from 'react' +import { useCallback, useMemo, useRef } from 'react' import { useSelector } from 'react-redux' +import { TaskRow } from '@/components/cards/TaskRow' + +import { PreviewMode } from '@/types/common' +import { sortTaskByDescendingOrder } from '@/utils/sortByDescending' + interface TasksVirtualizerProps { rows: TaskResponse[] mode: UserRole @@ -70,7 +75,9 @@ export function TasksRowVirtualizer({ rows, mode, token, subtasksByTaskId, workf
rowVirtualizer.measureElement(node)} + ref={(node) => { + rowVirtualizer.measureElement(node) + }} style={{ display: 'flex', position: 'absolute', @@ -118,101 +125,203 @@ export function TasksRowVirtualizer({ rows, mode, token, subtasksByTaskId, workf
) } +interface TasksListVirtualizerProps { + workflowStates: WorkflowStateResponse[] + mode: UserRole -export function TasksColumnVirtualizer({ - rows, + subtasksByTaskId: Record + filterTaskWithWorkflowStateId: (id: string) => TaskResponse[] + taskCountForWorkflowStateId: (id: string) => string + previewMode?: PreviewMode + onDropItem: (payload: { taskId: string; targetWorkflowStateId: string }) => void +} + +type VirtualItem = + | { + type: 'task' + task: TaskResponse + workflowState: WorkflowStateResponse + taskIndex: number + } + | { + type: 'subtask' + task: TaskResponse + parentTask: TaskResponse + } + +export function TasksListVirtualizer({ + workflowStates, mode, - list, subtasksByTaskId, -}: TasksVirtualizerProps & { list: WorkflowStateResponse }) { + filterTaskWithWorkflowStateId, + taskCountForWorkflowStateId, + previewMode, + onDropItem, +}: TasksListVirtualizerProps) { const { showSubtasks } = useSelector(selectTaskBoard) const { tokenPayload } = useSelector(selectAuthDetails) - const parentRef = useRef(null) - const columnVirtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => parentRef.current, + const scrollRef = useRef(null) + + const sections = useMemo(() => { + return workflowStates.map((workflowState) => { + const tasks = sortTaskByDescendingOrder(filterTaskWithWorkflowStateId(workflowState.id)) + + const items: VirtualItem[] = [] + tasks.forEach((task, taskIndex) => { + items.push({ + type: 'task', + task, + workflowState, + taskIndex, + }) + + if (showSubtasks) { + const subtasks = subtasksByTaskId[task.id] ?? [] + subtasks.forEach((subtask) => { + items.push({ + type: 'subtask', + task: subtask, + parentTask: task, + }) + }) + } + }) + + return { + workflowState, + items, + } + }) + }, [workflowStates, filterTaskWithWorkflowStateId, showSubtasks, subtasksByTaskId]) + + const allItems = useMemo(() => { + return sections.flatMap((section) => section.items) + }, [sections]) + + const virtualizer = useVirtualizer({ + count: allItems.length, + getScrollElement: () => scrollRef.current, estimateSize: () => 44, - measureElement: (element) => element.getBoundingClientRect().height, + measureElement: (el) => el.getBoundingClientRect().height, overscan: 100, }) + + const sectionRanges = useMemo(() => { + let start = 0 + return sections.map((section) => { + const end = start + section.items.length + const range = { start, end } + start = end + return range + }) + }, [sections]) + return (
-
- {columnVirtualizer.getVirtualItems().map((virtualRow) => { - const subtasks = showSubtasks ? (subtasksByTaskId[rows[virtualRow.index].id] ?? []) : [] - return ( -
columnVirtualizer.measureElement(node)} - style={{ - display: 'flex', - position: 'absolute', - transform: `translateY(${virtualRow.start}px)`, - width: '100%', - }} - draggable={!checkIfTaskViewer(rows[virtualRow.index].viewers, tokenPayload)} - onDragStart={(e) => { - if (checkIfTaskViewer(rows[virtualRow.index].viewers, tokenPayload)) { - e.preventDefault() - } - }} + {sections.map((section, sectionIndex) => { + const range = sectionRanges[sectionIndex] + const sectionItems = section.items + + return ( + + -
- <> - - - {showSubtasks && - subtasks?.length > 0 && - subtasks.map((subtask) => { - return ( - + {virtualizer + .getVirtualItems() + .filter((v) => v.index >= range.start && v.index < range.end) + .map((virtualRow) => { + const item = allItems[virtualRow.index] + const relativeIndex = virtualRow.index - range.start + const relativeStart = relativeIndex * 44 + + return ( +
+ {item.type === 'task' && ( +
{ + if (checkIfTaskViewer(item.task.viewers, tokenPayload)) { + e.preventDefault() + } }} - /> - ) - })} - - + > + + + +
+ )} + + {item.type === 'subtask' && ( +
+ +
+ )} +
+ ) + })}
-
- ) - })} -
+ + + ) + })}
) } diff --git a/src/components/atoms/CopilotTooltip.tsx b/src/components/atoms/CopilotTooltip.tsx index 02af20187..32e4466f7 100644 --- a/src/components/atoms/CopilotTooltip.tsx +++ b/src/components/atoms/CopilotTooltip.tsx @@ -4,7 +4,7 @@ import React, { useRef, useState } from 'react' export interface CopilotTooltipProps { content: React.ReactNode - children: React.ReactElement + children: React.ReactElement position?: 'top' | 'bottom' | 'left' | 'right' disabled?: boolean allowMaxWidth?: boolean diff --git a/src/components/atoms/TaskTitle.tsx b/src/components/atoms/TaskTitle.tsx index cad5b8761..3a6a135ce 100644 --- a/src/components/atoms/TaskTitle.tsx +++ b/src/components/atoms/TaskTitle.tsx @@ -57,13 +57,9 @@ const TaskTitle = ({ title, variant = 'board', isClient = false }: TaskTitleProp ) - const TaskTitleComponent = () => { - return {title} - } - return isOverflowing ? ( - } allowMaxWidth={true}> + } allowMaxWidth={true}> {typographyComponent} @@ -72,4 +68,8 @@ const TaskTitle = ({ title, variant = 'board', isClient = false }: TaskTitleProp ) } +const TaskTitleComponent = ({ title }: { title?: string }) => { + return {title} +} + export default TaskTitle diff --git a/src/components/atoms/Tooltip.tsx b/src/components/atoms/Tooltip.tsx index c25362a08..07ae181f1 100644 --- a/src/components/atoms/Tooltip.tsx +++ b/src/components/atoms/Tooltip.tsx @@ -3,7 +3,7 @@ import { ReactElement } from 'react' interface TooltipProps { title: string - children: ReactElement + children: ReactElement } const TooltipComponent = ({ title, children }: TooltipProps) => { diff --git a/src/components/buttonsGroup/FilterButtonsGroup.tsx b/src/components/buttonsGroup/FilterButtonsGroup.tsx index 88263198a..3baa036a1 100644 --- a/src/components/buttonsGroup/FilterButtonsGroup.tsx +++ b/src/components/buttonsGroup/FilterButtonsGroup.tsx @@ -1,5 +1,5 @@ -import { Stack, Typography } from '@mui/material' -import { TertiaryBtn } from '../buttons/TertiaryBtn' +import { Stack, Tab, Tabs } from '@mui/material' +import { FilterButtonsGroupSelector } from '@/components/inputs/FilterButtonsGroupSelector' export type FilterButtons = { name: string @@ -9,22 +9,41 @@ export type FilterButtons = { type FilterButtonGroupProps = { filterButtons: FilterButtons[] - activeButtonIndex: number | undefined + activeButtonIndex: number + mobileView?: boolean } -const FilterButtonGroup = ({ filterButtons, activeButtonIndex }: FilterButtonGroupProps) => { +const FilterButtonGroup = ({ filterButtons, activeButtonIndex, mobileView = false }: FilterButtonGroupProps) => { if (!filterButtons.length) { return null } + if (mobileView) { + return ( + + + + ) + } + return ( ({ - border: `1px solid ${theme.color.borders.border}`, - borderRadius: 1, columnGap: '8px', - padding: '4px 4px', - height: '32px', + padding: '4px 20px', + height: '48px', justifyContent: 'space-between', '@media (max-width: 330px)': { flexWrap: 'wrap', @@ -33,31 +52,56 @@ const FilterButtonGroup = ({ filterButtons, activeButtonIndex }: FilterButtonGro })} direction={'row'} > - {filterButtons.map((item, index) => { - return ( - (index === activeButtonIndex ? theme.color.gray[600] : theme.color.gray[500]), - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }} - > - {item.name} - - } - handleClick={() => item.onClick(index)} - outlined - enableBackground={index === activeButtonIndex} - /> - ) - })} + theme.color.gray[600], + height: '1px', + }, + }} + sx={{ + alignItems: 'center', + justifyContent: 'center', + minHeight: 'unset', + height: '46px', + '& .MuiTabs-flexContainer': { + gap: '12px', + }, + }} + > + {filterButtons.map((item, index) => { + return ( + item.onClick(index)} + sx={{ + textTransform: 'none', + fontSize: 14, + fontWeight: 400, + lineHeight: '22px', + minHeight: 'unset', + paddingY: 0, + paddingX: '4px', + paddingBottom: '4px', + height: '44px', + minWidth: '60px', + + fontColor: (theme) => theme.color.text, + color: (theme) => theme.color.text.text, + + '&.Mui-selected': { + color: (theme) => theme.color.text.text, + }, + }} + disableRipple + /> + ) + })} + ) } diff --git a/src/components/cards/CommentCard.tsx b/src/components/cards/CommentCard.tsx index c056f9f54..8b26ac56c 100644 --- a/src/components/cards/CommentCard.tsx +++ b/src/components/cards/CommentCard.tsx @@ -65,7 +65,7 @@ export const CommentCard = ({ const [replies, setReplies] = useState(comment.details.replies as ReplyResponse[]) const [timeAgo, setTimeAgo] = useState(getTimeDifference(comment.createdAt)) const [isReadOnly, setIsReadOnly] = useState(true) - const editRef = useRef(null) + const editRef = useRef(document.createElement('div')) const [focusReplyInput, setFocusedReplyInput] = useState(false) const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState(false) diff --git a/src/components/cards/ReplyCard.tsx b/src/components/cards/ReplyCard.tsx index 22a6f9a70..5f7418151 100644 --- a/src/components/cards/ReplyCard.tsx +++ b/src/components/cards/ReplyCard.tsx @@ -55,7 +55,7 @@ export const ReplyCard = ({ const [editedContent, setEditedContent] = useState(content) const [isListOrMenuActive, setIsListOrMenuActive] = useState(false) const [isFocused, setIsFocused] = useState(false) - const editRef = useRef(null) + const editRef = useRef(document.createElement('div')) const canEdit = tokenPayload?.internalUserId == item?.initiatorId || tokenPayload?.clientId == item?.initiatorId diff --git a/src/components/cards/TaskCard.tsx b/src/components/cards/TaskCard.tsx index 619185f86..d3dc9a004 100644 --- a/src/components/cards/TaskCard.tsx +++ b/src/components/cards/TaskCard.tsx @@ -56,6 +56,7 @@ import { import z from 'zod' import { StyledModal } from '@/app/detail/ui/styledComponent' import { ConfirmUI } from '@/components/layouts/ConfirmUI' +import { useRouter } from 'next/navigation' const TaskCardContainer = styled(Stack)(({ theme }) => ({ border: `1px solid ${theme.color.borders.border}`, @@ -103,6 +104,11 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi const [currentDueDate, setCurrentDueDate] = useState(task.dueDate) const [selectedAssignee, setSelectedAssignee] = useState(undefined) + const router = useRouter() + + const [assigneeValue, setAssigneeValue] = useState | undefined>(() => { + return assigneeCache[task.id] + }) //Omitting type for NoAssignee const { renderingItem: _statusValue, updateRenderingItem: updateStatusValue } = useHandleSelectorComponent({ // item: selectedWorkflowState, @@ -164,10 +170,6 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi return match ?? NoAssignee } - const [assigneeValue, setAssigneeValue] = useState | undefined>(() => { - return assigneeCache[task.id] - }) //Omitting type for NoAssignee - return ( @@ -211,7 +213,7 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi })()} onChange={handleAssigneeChange} tooltipProps={{ - content: assigneeValue === NoAssignee ? 'Set assignee' : 'Change assignee', + content: assigneeValue === NoAssignee ? 'Set assignee' : getAssigneeName(assigneeValue), disabled: mode === UserRole.Client && !previewMode, }} variant="icon" @@ -298,22 +300,30 @@ export const TaskCard = ({ task, href, workflowState, mode, subtasks, workflowDi {showSubtasks && subtasks && subtasks.length > 0 && ( {subtasks.map((subtask) => { + const href = `${getCardHref(subtask, mode)}/?token=${token}` return ( - - theme.color.gray[150], - }, - }} - > - - - + theme.color.gray[150], + }, + }} + onMouseEnter={(e) => { + e.preventDefault() + router.prefetch(href) + }} + onClick={(e) => { + e.preventDefault() + router.push(href) + }} //removed customLink and applied manual prefetch for now. todo: prevent nested Links in TaskCard. + > + + ) })} diff --git a/src/components/cards/TaskRow.tsx b/src/components/cards/TaskRow.tsx index 1113ae8e0..d3bccc4b5 100644 --- a/src/components/cards/TaskRow.tsx +++ b/src/components/cards/TaskRow.tsx @@ -5,15 +5,18 @@ import { Typography, Box, Stack } from '@mui/material' import { AddBtn } from '@/components/buttons/AddBtn' import { handleAddBtnClicked } from '@/app/ui/TaskBoard.helpers' import { TaskWorkflowStateProps } from '@/types/taskBoard' -import { UserRole } from '@/app/api/core/types/user' +import { StateType } from '@prisma/client' +import { WorkflowStateThemeMap } from '@/types/objectMaps' interface TaskRowProps extends TaskWorkflowStateProps { + workflowStateType: StateType display?: boolean showAddBtn?: boolean } export const TaskRow = ({ workflowStateId, + workflowStateType, mode, children, columnName, @@ -25,7 +28,16 @@ export const TaskRow = ({ theme.color.gray[100], + position: 'sticky', + top: 0, + zIndex: 100, + background: (theme) => ` + linear-gradient( + ${theme.color.workflowState[WorkflowStateThemeMap[workflowStateType]]}, + ${theme.color.workflowState[WorkflowStateThemeMap[workflowStateType]]} + ), + ${theme.palette.background.paper} + `, borderBottom: (theme) => `1px solid ${theme.color.borders.borderDisabled}`, }} > diff --git a/src/components/inputs/CopilotSelector.tsx b/src/components/inputs/CopilotSelector.tsx index d58770ce5..9fff672c1 100644 --- a/src/components/inputs/CopilotSelector.tsx +++ b/src/components/inputs/CopilotSelector.tsx @@ -93,6 +93,10 @@ export const CopilotPopSelector = ({ const shouldCallOnChangeWithEmpty = useRef(false) const initialAssignee = initialValue && parseAssigneeToSelectorOptions(initialValue) + const [selectedAssignee, setSelectedAssignee] = useState( + initialAssignee && selectorOptionsToInputValue(initialAssignee), + ) + const handleClick = useCallback( (event: React.MouseEvent) => { event.preventDefault() @@ -106,9 +110,6 @@ export const CopilotPopSelector = ({ [disabled, initialAssignee], ) - const [selectedAssignee, setSelectedAssignee] = useState( - initialAssignee && selectorOptionsToInputValue(initialAssignee), - ) const handleClose = useCallback(() => { if (shouldCallOnChangeWithEmpty.current && selectedAssignee && !selectedAssignee.length) { onChange(selectedAssignee) diff --git a/src/components/inputs/FilterButtonsGroupSelector/index.tsx b/src/components/inputs/FilterButtonsGroupSelector/index.tsx new file mode 100644 index 000000000..d3e3eff3c --- /dev/null +++ b/src/components/inputs/FilterButtonsGroupSelector/index.tsx @@ -0,0 +1,130 @@ +import { FilterButtons } from '@/components/buttonsGroup/FilterButtonsGroup' +import { DropdownIcon } from '@/icons' +import { Box, ClickAwayListener, Popper, Stack, Typography } from '@mui/material' +import { useState } from 'react' + +export const FilterButtonsGroupSelector = ({ + filterButtons, + activeButtonIndex, +}: { + filterButtons: FilterButtons[] + activeButtonIndex: number +}) => { + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + const id = open ? 'filterButtons-selector-popper' : '' + + const handleKeyDown = (event: React.KeyboardEvent) => { + event.stopPropagation() + if (event.key === 'Escape') { + setAnchorEl(null) + } + } + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl((prev) => (prev ? null : event.currentTarget)) + } + + return ( + { + setAnchorEl(null) + }} + > + { + handleClick(e) + }} + > + {filterButtons[activeButtonIndex].name} + + + + { + e.stopPropagation() + }} + modifiers={[ + { + name: 'offset', + options: { + offset: [0, -2], + }, + }, + ]} + > + theme.color.base.white, + border: (theme) => `1px solid ${theme.color.gray[150]}`, + borderRadius: '4px', + display: 'flex', + width: '134px', + padding: '4px 0px', + flexDirection: 'column', + alignItems: 'flex-start', + overflow: 'hidden', + }} + rowGap={'2px'} + > + {filterButtons.map((item, index) => { + return ( + ({ + display: 'flex', + alignItems: 'center', + padding: '4px 16px 4px 12px', + lineHeight: '22px', + gap: '8px', + color: theme.color.text.textPrimary, + alignSelf: 'stretch', + background: 'white', + ':hover': { + cursor: 'pointer', + background: theme.color.gray[100], + }, + })} + onClick={() => { + item.onClick(index) + setAnchorEl(null) + }} + > + + {item.name} + + + ) + })} + + + + + ) +} diff --git a/src/components/inputs/FilterSelector/index.tsx b/src/components/inputs/FilterSelector/index.tsx index 7b0e32718..4a987357a 100644 --- a/src/components/inputs/FilterSelector/index.tsx +++ b/src/components/inputs/FilterSelector/index.tsx @@ -90,7 +90,7 @@ export const FilterSelector = ({ disabled }: FilterSelectorProps) => { ({ color: theme.color.text.textSecondary, - fontSize: theme.typography.bodySm, + fontSize: '13px', lineHeight: '21px', })} > diff --git a/src/components/inputs/ListboxComponent.tsx b/src/components/inputs/ListboxComponent.tsx index 3dffe82e3..d76547a5b 100644 --- a/src/components/inputs/ListboxComponent.tsx +++ b/src/components/inputs/ListboxComponent.tsx @@ -1,4 +1,3 @@ -import CustomScrollBar from '@/hoc/CustomScrollBar' import { Box } from '@mui/material' import { useRouter } from 'next/navigation' @@ -12,7 +11,8 @@ type ListboxComponentProps = React.HTMLAttributes & { } const ListboxComponent = React.forwardRef((props, ref) => { - const { children, endOption, endOptionHref, role } = props + const { children, endOption, endOptionHref, ...otherProps } = props + const router = useRouter() return ( maxHeight: 'none', }} > - - + + {children} {endOption && ( endOptionHref && router.push(endOptionHref)}> diff --git a/src/components/inputs/ReplyInput.tsx b/src/components/inputs/ReplyInput.tsx index 32944c09c..33d31628c 100644 --- a/src/components/inputs/ReplyInput.tsx +++ b/src/components/inputs/ReplyInput.tsx @@ -98,7 +98,7 @@ export const ReplyInput = ({ setIsUploading(uploading) } - const editorRef = useRef(null) + const editorRef = useRef(document.createElement('div')) const [isMultiline, setIsMultiline] = useState(false) useEffect(() => { diff --git a/src/components/inputs/Selector.tsx b/src/components/inputs/Selector.tsx index 0681e1ac3..c36987da0 100644 --- a/src/components/inputs/Selector.tsx +++ b/src/components/inputs/Selector.tsx @@ -1,8 +1,8 @@ -import { Box, Button, Popper, Stack, Typography } from '@mui/material' +import { Box, Button, Popper, Stack, SxProps, Theme, Typography } from '@mui/material' import { StyledAutocomplete } from '@/components/inputs/Autocomplete' import { statusIcons } from '@/utils/iconMatcher' import { useFocusableInput } from '@/hooks/useFocusableInput' -import { HTMLAttributes, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { HTMLAttributes, ReactNode, useCallback, useEffect, useMemo, useRef, useState, type JSX } from 'react' import { StyledTextField } from './TextField' import { WorkflowStateResponse } from '@/types/dto/workflowStates.dto' import { IAssigneeCombined, Sizes, IExtraOption, ITemplate, UserTypesName } from '@/types/interfaces' @@ -66,6 +66,7 @@ interface Prop { cursor?: Property.Cursor currentOption?: SelectorOptionsType[T] //option which shall be at the top of the selector without any grouping errorPlaceholder?: string + customDropdownWidth?: number } export default function Selector({ @@ -98,6 +99,7 @@ export default function Selector({ cursor, currentOption, errorPlaceholder = 'Required', + customDropdownWidth, }: Prop) { const [anchorEl, setAnchorEl] = useState(null) @@ -142,7 +144,7 @@ export default function Selector({ } const [placement, setPlacement] = useState('bottom') - const timeoutRef = useRef() + const timeoutRef = useRef(undefined) const handlePlacementChange = useCallback((data: ModifierArguments) => { if (data.state?.placement && data.state.placement !== placement) { @@ -199,18 +201,6 @@ export default function Selector({ } } - const ListWithEndOption = React.forwardRef((props, ref) => { - const { ...other } = props - - return ( - - {props.children} - - ) - }) - - ListWithEndOption.displayName = 'ListWithEndOption' - return ( @@ -271,7 +261,7 @@ export default function Selector({ open={open} anchorEl={anchorEl} sx={{ - width: 'fit-content', + width: customDropdownWidth ? `${customDropdownWidth}px` : 'fit-content', zIndex: '9999', }} placement="bottom-start" @@ -286,24 +276,28 @@ export default function Selector({ openOnFocus onKeyDown={handleKeyDown} autoSelect={false} - ListboxProps={{ - sx: { - maxHeight: { - xs: '175px', - sm: '291px', - }, - '& .MuiAutocomplete-option[aria-selected="true"].Mui-focused': { - backgroundColor: (theme) => theme.color.background.bgHover, - }, - '& .MuiAutocomplete-option[aria-selected="true"]': { - backgroundColor: (theme) => theme.color.base.white, - }, - '& .MuiAutocomplete-option.Mui-focused': { - backgroundColor: (theme) => theme.color.background.bgHover, + ListboxProps={ + { + endOption: endOption, + endOptionHref: endOptionHref, + sx: { + maxHeight: { + xs: '175px', + sm: '291px', + }, + '& .MuiAutocomplete-option[aria-selected="true"].Mui-focused': { + backgroundColor: (theme) => theme.color.background.bgHover, + }, + '& .MuiAutocomplete-option[aria-selected="true"]': { + backgroundColor: (theme) => theme.color.base.white, + }, + '& .MuiAutocomplete-option.Mui-focused': { + backgroundColor: (theme) => theme.color.background.bgHover, + }, + padding: '0px', }, - padding: '0px', - }, - }} + } as ExtendedListboxProps + } options={extraOption ? [extraOption, ...processedOptions] : processedOptions} value={value} onChange={(_, newValue: unknown) => { @@ -349,11 +343,11 @@ export default function Selector({ return params.children } return ( - <> + {hasNoAssignee ? ( - {params.children} + {params.children} ) : ( - + ({ {params.children} )} - + ) }} inputValue={inputStatusValue} @@ -385,7 +379,7 @@ export default function Selector({ padding="4px 12px 8px 12px" basePadding="8px 12px 8px 12px" sx={{ - width: '200px', + width: customDropdownWidth ? `${customDropdownWidth}px` : '200px', visibility: { xs: 'none', sm: 'visible' }, borderRadius: '4px', }} @@ -406,9 +400,11 @@ export default function Selector({ setAnchorEl(null) setInputStatusValue('') } + const { key, ...otherProps } = props if ((option as IExtraOption).id === 'not_found') { return ( ({ ) } - return extraOption && extraOptionRenderer && (option as IExtraOption)?.extraOptionFlag ? ( - extraOptionRenderer(setAnchorEl, anchorEl, props) - ) : selectorType === SelectorType.ASSIGNEE_SELECTOR ? ( - - ) : selectorType === SelectorType.STATUS_SELECTOR ? ( - - ) : selectorType === SelectorType.TEMPLATE_SELECTOR ? ( - - ) : ( - <> - ) + if (extraOption && extraOptionRenderer && (option as IExtraOption)?.extraOptionFlag) { + return
{extraOptionRenderer(setAnchorEl, anchorEl, otherProps)}
+ } + + if (selectorType === SelectorType.ASSIGNEE_SELECTOR) { + return + } + + if (selectorType === SelectorType.STATUS_SELECTOR) { + return + } + + if (selectorType === SelectorType.TEMPLATE_SELECTOR) { + return ( + + ) + } + + return <> }} noOptionsText={endOption && } /> @@ -453,14 +459,15 @@ const TemplateSelectorRenderer = ({ props, option, onClickHandler, + width, }: { props: HTMLAttributes option: unknown onClickHandler?: () => void + width?: number }) => { return ( {(option as ITemplate).title as string}
@@ -552,3 +564,24 @@ const AssigneeSelectorRenderer = ({ props, option }: { props: HTMLAttributes ) } + +const ListWithEndOption = React.forwardRef< + HTMLDivElement, + JSX.IntrinsicElements['div'] & { endOption?: React.ReactNode; endOptionHref?: string } +>((props, ref) => { + const { endOption, endOptionHref, ...other } = props + + return ( + + {props.children} + + ) +}) + +ListWithEndOption.displayName = 'ListWithEndOption' + +type ExtendedListboxProps = React.HTMLAttributes & { + sx?: SxProps + endOption?: React.ReactNode + endOptionHref?: string +} diff --git a/src/components/layouts/FilterBar.tsx b/src/components/layouts/FilterBar.tsx index 80cddb55b..687542270 100644 --- a/src/components/layouts/FilterBar.tsx +++ b/src/components/layouts/FilterBar.tsx @@ -88,15 +88,27 @@ export const FilterBar = ({ mode }: FilterBarProps) => { > - + - + { @@ -150,21 +162,22 @@ export const FilterBar = ({ mode }: FilterBarProps) => { - - - - + + + diff --git a/src/hoc/CustomScrollBar.tsx b/src/hoc/CustomScrollBar.tsx index 7151895e5..67baae539 100644 --- a/src/hoc/CustomScrollBar.tsx +++ b/src/hoc/CustomScrollBar.tsx @@ -25,8 +25,8 @@ export const CustomScrollBar: React.FC = ({ children, clas const [isScrollable, setIsScrollable] = useState(false) const [dragStartY, setDragStartY] = useState(0) const [startScrollTop, setStartScrollTop] = useState(0) - const scrollTimeoutRef = useRef() - const resizeObserverRef = useRef() + const scrollTimeoutRef = useRef(undefined) + const resizeObserverRef = useRef(undefined) const scrollHostRef = useRef(null) diff --git a/src/hoc/RealtimeTemplates.tsx b/src/hoc/RealtimeTemplates.tsx index c0c83d148..69877138c 100644 --- a/src/hoc/RealtimeTemplates.tsx +++ b/src/hoc/RealtimeTemplates.tsx @@ -1,5 +1,6 @@ 'use client' +import { RealtimeTemplatesHandler } from '@/lib/realtimeTemplates' import { supabase } from '@/lib/supabase' import { selectCreateTemplate, setActiveTemplate, setTemplates } from '@/redux/features/templateSlice' import store from '@/redux/store' @@ -28,37 +29,11 @@ export const RealTimeTemplates = ({ tokenPayload: Token token: string }) => { - const { templates = [], activeTemplate } = useSelector(selectCreateTemplate) + const { templates = [] } = useSelector(selectCreateTemplate) const pathname = usePathname() const router = useRouter() - const applySubtemplateToParentTemplate = (newTemplate: RealTimeTemplateResponse) => { - if (!newTemplate?.parentId) return - - if (activeTemplate?.id === newTemplate.parentId) { - store.dispatch( - setActiveTemplate({ - ...activeTemplate, - subTaskTemplates: [...(activeTemplate.subTaskTemplates || []), newTemplate], - }), - ) - } - - store.dispatch( - setTemplates( - templates.map((template) => - template.id === newTemplate.parentId - ? { - ...template, - subTaskTemplates: [...(template.subTaskTemplates || []), newTemplate], - } - : template, - ), - ), - ) //also append the subTaskTemplates to parent template on the templates store. - } - const redirectBack = (updatedTemplate: RealTimeTemplateResponse) => { //disable board navigation if not in template details page if (!pathname.includes(`manage-templates/${updatedTemplate.id}`)) return @@ -74,64 +49,23 @@ export const RealTimeTemplates = ({ if (isTemplatePayloadEqual(payload)) { return //no changes for the same payload } + + const realtimeHandler = new RealtimeTemplatesHandler(payload, redirectBack, tokenPayload) + const isSubTemplate = + Object.keys(payload.new).includes('parentId') && (payload.new as RealTimeTemplateResponse).parentId !== null + + if (isSubTemplate) { + return realtimeHandler.handleRealtimeSubTemplates() + } + if (payload.eventType === 'INSERT') { - const newTemplate = getFormattedTemplate(payload.new) - let canUserAccessTask = newTemplate.workspaceId === tokenPayload.workspaceId - if (!canUserAccessTask) return - if (newTemplate?.parentId) { - applySubtemplateToParentTemplate(newTemplate) - return - } - templates - ? store.dispatch(setTemplates([{ ...newTemplate }, ...templates])) - : store.dispatch(setTemplates([{ ...newTemplate }])) + return realtimeHandler.handleRealtimeTemplateInsert() } if (payload.eventType === 'UPDATE') { - const updatedTemplate = getFormattedTemplate(payload.new) - - const oldTemplate = templates && templates.find((template) => template.id == updatedTemplate.id) - if (payload.new.workspaceId === tokenPayload.workspaceId) { - if (updatedTemplate.deletedAt) { - const newTemplateArr = templates && templates.filter((el) => el.id !== updatedTemplate.id) - store.dispatch(setTemplates(newTemplateArr)) - redirectBack(updatedTemplate) - } else { - // Address Postgres' 8kb pagesize limitation (See TOAST https://www.postgresql.org/docs/current/storage-toast.html) - // If `body` field (which can be larger than pagesize) is not changed, Supabase Realtime won't send large fields like this in `payload.new` - - // So, we need to check if the oldTask has valid body but new body field is not being sent in updatedTask, and add it if required - if (oldTemplate?.body && updatedTemplate.body === undefined) { - updatedTemplate.body = oldTemplate?.body - } - if (oldTemplate && oldTemplate.body && updatedTemplate.body) { - const oldImgSrcs = extractImgSrcs(oldTemplate.body) - const newImgSrcs = extractImgSrcs(updatedTemplate.body) - // Need to extract new image Srcs and replace it with old ones, because since we are creating a new url of images on each task details navigation, - // a second user navigating the task details will generate a new src and replace it in the database which causes the previous user to load the src again(because its new) - if (oldImgSrcs.length > 0 && newImgSrcs.length > 0) { - updatedTemplate.body = replaceImgSrcs(updatedTemplate.body, newImgSrcs, oldImgSrcs) - } - } - if (activeTemplate?.id == updatedTemplate.id) { - store.dispatch( - setActiveTemplate({ - ...updatedTemplate, - subTaskTemplates: activeTemplate.subTaskTemplates, - }), - ) - } - if (updatedTemplate?.parentId) { - applySubtemplateToParentTemplate(updatedTemplate) - return - } - const newTemplateArr = [ - updatedTemplate, - ...(templates?.filter((template) => template.id !== updatedTemplate.id) || []), - ] - store.dispatch(setTemplates(newTemplateArr)) - } - } + return realtimeHandler.handleRealtimeTemplateUpdate() } + + console.error('Unknown event type for realtime handler') } useEffect(() => { diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts index e32f4bd7e..ab225589b 100644 --- a/src/hooks/useClickOutside.ts +++ b/src/hooks/useClickOutside.ts @@ -21,7 +21,7 @@ function getEventPath(event: Event): EventTarget[] { * @param events - Event types to listen to (default: ["pointerdown", "touchstart"]). */ export default function useClickOutside( - refs: RefObject | RefObject[], + refs: RefObject | RefObject[], handler: (event: Event) => void, events: string[] = ['pointerdown', 'touchstart'], ): void { diff --git a/src/icons/dropdown.svg b/src/icons/dropdown.svg new file mode 100644 index 000000000..6a9a02c6e --- /dev/null +++ b/src/icons/dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/index.ts b/src/icons/index.ts index 8bf5b92a4..c405e58bf 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -96,3 +96,4 @@ export { default as InProgressIconSmall } from './InProgressSmall.svg' export { default as CompletedIconSmall } from './CompletedSmall.svg' export { default as SubtaskIcon } from './subtask.svg' export { default as InfoIcon } from './info.svg' +export { default as DropdownIcon } from './dropdown.svg' diff --git a/src/lib/realtimeTemplates.ts b/src/lib/realtimeTemplates.ts new file mode 100644 index 000000000..5a1c217f5 --- /dev/null +++ b/src/lib/realtimeTemplates.ts @@ -0,0 +1,119 @@ +'use client' + +import { RealTimeTemplateResponse } from '@/hoc/RealtimeTemplates' +import { selectCreateTemplate, setActiveTemplate, setTemplates } from '@/redux/features/templateSlice' +import store from '@/redux/store' +import { Token } from '@/types/common' +import { AssigneeType, IAssigneeCombined } from '@/types/interfaces' +import { getFormattedTemplate } from '@/utils/getFormattedRealTimeData' +import { extractImgSrcs, replaceImgSrcs } from '@/utils/signedUrlReplacer' +import { RealtimePostgresChangesPayload } from '@supabase/supabase-js' + +export class RealtimeTemplatesHandler { + constructor( + private readonly payload: RealtimePostgresChangesPayload, + private readonly redirectToTemplateBoard: (newTemplate: RealTimeTemplateResponse) => void, + private readonly tokenPayload: Token, + ) { + const newTemplate = getFormattedTemplate(this.payload.new) + if (newTemplate.workspaceId !== this.tokenPayload.workspaceId) { + console.error('Realtime event ignored for template with different workspaceId') + return + } + } + + handleRealtimeTemplateInsert() { + const currentState = store.getState() + const { templates } = selectCreateTemplate(currentState) + const newTemplate = getFormattedTemplate(this.payload.new) + + templates + ? store.dispatch(setTemplates([{ ...newTemplate }, ...templates])) + : store.dispatch(setTemplates([{ ...newTemplate }])) + } + + handleRealtimeTemplateUpdate() { + const updatedTemplate = getFormattedTemplate(this.payload.new) + const currentState = store.getState() + const { templates, activeTemplate } = selectCreateTemplate(currentState) + const oldTemplate = templates && templates.find((template) => template.id == updatedTemplate.id) + + //handle template deleted case. + if (updatedTemplate.deletedAt) { + const newTemplateArr = templates && templates.filter((el) => el.id !== updatedTemplate.id) + store.dispatch(setTemplates(newTemplateArr)) + this.redirectToTemplateBoard(updatedTemplate) + } else { + // Address Postgres' 8kb pagesize limitation (See TOAST https://www.postgresql.org/docs/current/storage-toast.html) + // If `body` field (which can be larger than pagesize) is not changed, Supabase Realtime won't send large fields like this in `payload.new` + + // So, we need to check if the oldTask has valid body but new body field is not being sent in updatedTask, and add it if required + if (oldTemplate?.body && updatedTemplate.body === undefined) { + updatedTemplate.body = oldTemplate?.body + } + if (oldTemplate && oldTemplate.body && updatedTemplate.body) { + const oldImgSrcs = extractImgSrcs(oldTemplate.body) + const newImgSrcs = extractImgSrcs(updatedTemplate.body) + // Need to extract new image Srcs and replace it with old ones, because since we are creating a new url of images on each task details navigation, + // a second user navigating the task details will generate a new src and replace it in the database which causes the previous user to load the src again(because its new) + if (oldImgSrcs.length > 0 && newImgSrcs.length > 0) { + updatedTemplate.body = replaceImgSrcs(updatedTemplate.body, newImgSrcs, oldImgSrcs) + } + } + if (activeTemplate?.id == updatedTemplate.id) { + store.dispatch( + setActiveTemplate({ + ...updatedTemplate, + subTaskTemplates: activeTemplate.subTaskTemplates, + }), + ) + } + if (updatedTemplate?.parentId) { + this.applySubtemplateToParentTemplate(updatedTemplate) + return + } + const newTemplateArr = [ + updatedTemplate, + ...(templates?.filter((template) => template.id !== updatedTemplate.id) || []), + ] + store.dispatch(setTemplates(newTemplateArr)) + } + } + + handleRealtimeSubTemplates() { + if (this.payload.eventType === 'INSERT') { + const newTemplate = getFormattedTemplate(this.payload.new) + this.applySubtemplateToParentTemplate(newTemplate) + } else { + this.handleRealtimeTemplateUpdate() + } + } + + private applySubtemplateToParentTemplate(newTemplate: RealTimeTemplateResponse) { + if (!newTemplate?.parentId) return + const currentState = store.getState() + const { templates, activeTemplate } = selectCreateTemplate(currentState) + + if (activeTemplate?.id === newTemplate.parentId) { + store.dispatch( + setActiveTemplate({ + ...activeTemplate, + subTaskTemplates: [...(activeTemplate.subTaskTemplates || []), newTemplate], + }), + ) + } + + store.dispatch( + setTemplates( + templates.map((template) => + template.id === newTemplate.parentId + ? { + ...template, + subTaskTemplates: [...(template.subTaskTemplates || []), newTemplate], + } + : template, + ), + ), + ) //also append the subTaskTemplates to parent template on the templates store. + } +} diff --git a/src/theme/theme.ts b/src/theme/theme.ts index 53f8cbad2..317226da1 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -40,6 +40,11 @@ export const theme = createTheme({ '6xl': '120rem', }, color: { + workflowState: { + todo: '#90959d0a', + inProgress: '#8638050a', + done: '#115b3b0a', + }, base: { black: '#000000', white: '#ffffff', diff --git a/src/types/objectMaps.ts b/src/types/objectMaps.ts index e82aa51fd..fb55a4951 100644 --- a/src/types/objectMaps.ts +++ b/src/types/objectMaps.ts @@ -1,4 +1,6 @@ import { FilterByOptions, FilterOptionsKeywords, IAssigneeCombined, UserIds } from '@/types/interfaces' +import { Theme } from '@mui/material' +import { StateType } from '@prisma/client' export const filterTypeToButtonIndexMap: Record = { [FilterOptionsKeywords.CLIENTS]: 2, @@ -34,3 +36,11 @@ export const userIdFieldMap = { client: UserIds.CLIENT_ID, company: UserIds.COMPANY_ID, } as const + +export const WorkflowStateThemeMap: Record = { + backlog: 'todo', + unstarted: 'todo', + started: 'inProgress', + completed: 'done', + cancelled: 'todo', +} diff --git a/src/types/theme.d.ts b/src/types/theme.d.ts index 19366ee48..47279956d 100644 --- a/src/types/theme.d.ts +++ b/src/types/theme.d.ts @@ -38,6 +38,11 @@ export declare module '@mui/material/styles' { '6xl': string } color: { + workflowState: { + todo: string + inProgress: string + done: string + } base: { black: string white: string @@ -130,6 +135,11 @@ export declare module '@mui/material/styles' { '6xl': string } color: { + workflowState: { + todo: React.CSSProperties['color'] + inProgress: React.CSSProperties['color'] + done: React.CSSProperties['color'] + } base: { black: React.CSSProperties['color'] white: React.CSSProperties['color'] diff --git a/tsconfig.json b/tsconfig.json index 3b5d3b379..ea6b22f9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "plugins": [ { @@ -24,7 +24,7 @@ "@cmd/*": ["./src/cmd/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "trigger.config.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "trigger.config.ts", ".next/dev/types/**/*.ts"], "exclude": ["node_modules"], "types": ["jest"] } diff --git a/yarn.lock b/yarn.lock index 9bb0c3c58..36af531a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.24.4": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.28.3", "@babel/generator@^7.7.2": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" @@ -64,6 +85,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": version "7.27.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" @@ -193,6 +225,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-option@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" @@ -222,6 +259,13 @@ dependencies: "@babel/types" "^7.28.4" +"@babel/parser@^7.24.4", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== + dependencies: + "@babel/types" "^7.28.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" @@ -983,6 +1027,19 @@ "@babel/types" "^7.28.4" debug "^4.3.1" +"@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" + "@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.28.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" @@ -991,6 +1048,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.27.1" +"@babel/types@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1067,6 +1132,13 @@ dependencies: tslib "^2.4.0" +"@emnapi/runtime@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" + integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== + dependencies: + tslib "^2.4.0" + "@emnapi/wasi-threads@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" @@ -1328,37 +1400,73 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f" integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ== -"@eslint-community/eslint-utils@^4.2.0": +"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3" integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.6.1": - version "4.12.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" - integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== +"@eslint/config-array@^0.21.1": + version "0.21.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713" + integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA== + dependencies: + "@eslint/object-schema" "^2.1.7" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda" + integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== + dependencies: + "@eslint/core" "^0.17.0" + +"@eslint/core@^0.17.0": + version "0.17.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c" + integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.1", "@eslint/eslintrc@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac" + integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^4.1.0" + js-yaml "^4.1.1" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.57.1": - version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" - integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@eslint/js@9.39.2": + version "9.39.2" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" + integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== + +"@eslint/object-schema@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" + integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== + +"@eslint/plugin-kit@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2" + integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== + dependencies: + "@eslint/core" "^0.17.0" + levn "^0.4.1" "@faker-js/faker@^8.4.1": version "8.4.1" @@ -1395,24 +1503,175 @@ resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-4.0.0.tgz#e179893a3ad628b17a6fabdfcc9d468753aac11a" integrity sha512-1TUx3KdaU3cN7nfCdNf+UVqA/PSX29Cjcox3fZZBtINlRrXVTmUkQnCKv2MbBUbCopbK4olAT1IHl76uZyCiVA== -"@humanwhocodes/config-array@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" - integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.7" + resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== dependencies: - "@humanwhocodes/object-schema" "^2.0.3" - debug "^4.3.1" - minimatch "^3.0.5" + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.4.0" "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@img/colour@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@img/colour/-/colour-1.0.0.tgz#d2fabb223455a793bf3bf9c70de3d28526aa8311" + integrity sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw== + +"@img/sharp-darwin-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz#6e0732dcade126b6670af7aa17060b926835ea86" + integrity sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.2.4" + +"@img/sharp-darwin-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz#19bc1dd6eba6d5a96283498b9c9f401180ee9c7b" + integrity sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.2.4" + +"@img/sharp-libvips-darwin-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz#2894c0cb87d42276c3889942e8e2db517a492c43" + integrity sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g== + +"@img/sharp-libvips-darwin-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz#e63681f4539a94af9cd17246ed8881734386f8cc" + integrity sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg== + +"@img/sharp-libvips-linux-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz#b1b288b36864b3bce545ad91fa6dadcf1a4ad318" + integrity sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw== + +"@img/sharp-libvips-linux-arm@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz#b9260dd1ebe6f9e3bdbcbdcac9d2ac125f35852d" + integrity sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A== + +"@img/sharp-libvips-linux-ppc64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz#4b83ecf2a829057222b38848c7b022e7b4d07aa7" + integrity sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA== + +"@img/sharp-libvips-linux-riscv64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz#880b4678009e5a2080af192332b00b0aaf8a48de" + integrity sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA== + +"@img/sharp-libvips-linux-s390x@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz#74f343c8e10fad821b38f75ced30488939dc59ec" + integrity sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ== + +"@img/sharp-libvips-linux-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz#df4183e8bd8410f7d61b66859a35edeab0a531ce" + integrity sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw== + +"@img/sharp-libvips-linuxmusl-arm64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz#c8d6b48211df67137541007ee8d1b7b1f8ca8e06" + integrity sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw== + +"@img/sharp-libvips-linuxmusl-x64@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz#be11c75bee5b080cbee31a153a8779448f919f75" + integrity sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg== + +"@img/sharp-linux-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz#7aa7764ef9c001f15e610546d42fce56911790cc" + integrity sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.2.4" + +"@img/sharp-linux-arm@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz#5fb0c3695dd12522d39c3ff7a6bc816461780a0d" + integrity sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.2.4" + +"@img/sharp-linux-ppc64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz#9c213a81520a20caf66978f3d4c07456ff2e0813" + integrity sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA== + optionalDependencies: + "@img/sharp-libvips-linux-ppc64" "1.2.4" + +"@img/sharp-linux-riscv64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz#cdd28182774eadbe04f62675a16aabbccb833f60" + integrity sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw== + optionalDependencies: + "@img/sharp-libvips-linux-riscv64" "1.2.4" + +"@img/sharp-linux-s390x@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz#93eac601b9f329bb27917e0e19098c722d630df7" + integrity sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.2.4" + +"@img/sharp-linux-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz#55abc7cd754ffca5002b6c2b719abdfc846819a8" + integrity sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.2.4" + +"@img/sharp-linuxmusl-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz#d6515ee971bb62f73001a4829b9d865a11b77086" + integrity sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + +"@img/sharp-linuxmusl-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz#d97978aec7c5212f999714f2f5b736457e12ee9f" + integrity sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + +"@img/sharp-wasm32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz#2f15803aa626f8c59dd7c9d0bbc766f1ab52cfa0" + integrity sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw== + dependencies: + "@emnapi/runtime" "^1.7.0" + +"@img/sharp-win32-arm64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz#3706e9e3ac35fddfc1c87f94e849f1b75307ce0a" + integrity sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g== + +"@img/sharp-win32-ia32@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz#0b71166599b049e032f085fb9263e02f4e4788de" + integrity sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg== + +"@img/sharp-win32-x64@0.34.5": + version "0.34.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" + integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -1827,107 +2086,102 @@ resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.32.tgz#6d1107e2b7cc8649ff3730b8b46deb4e8a6d38fa" integrity sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng== -"@next/env@14.2.35": - version "14.2.35" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.35.tgz#e979016d0ca8500a47d41ffd02625fe29b8df35a" - integrity sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ== +"@next/env@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/env/-/env-16.1.1.tgz#3d06a470efff135746ef609cc02a4996512bd9ab" + integrity sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA== -"@next/eslint-plugin-next@14.0.4": - version "14.0.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.4.tgz#474fd88d92209270021186043513fbdc4203f5ec" - integrity sha512-U3qMNHmEZoVmHA0j/57nRfi3AscXNvkOnxDmle/69Jz/G0o/gWjXTDdlgILZdrxQ0Lw/jv2mPW8PGy0EGIHXhQ== +"@next/eslint-plugin-next@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz#544e1ca4775287e2369f7db6b8e81c752f35035f" + integrity sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw== dependencies: - glob "7.1.7" + fast-glob "3.3.1" "@next/swc-darwin-arm64@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.32.tgz#83482a7282df899b73d916e02b02a189771e706c" integrity sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw== -"@next/swc-darwin-arm64@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz#9e74a4223f1e5e39ca4f9f85709e0d95b869b298" - integrity sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA== +"@next/swc-darwin-arm64@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz#1e7e87fd21fcabce546dfb04fb946ecd9f866917" + integrity sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA== "@next/swc-darwin-x64@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.32.tgz#1a9eb676a014e1fc999251f10288c25a0f81d6d1" integrity sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg== -"@next/swc-darwin-x64@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz#fcf0c45938da9b0cc2ec86357d6aefca90bd17f3" - integrity sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA== +"@next/swc-darwin-x64@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz#6617d03b96bdffad7bf4df50d4a699faea0d04c3" + integrity sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw== "@next/swc-linux-arm64-gnu@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.32.tgz#7713a49abd555d6f698e766b1631b67d881b4ee4" integrity sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA== -"@next/swc-linux-arm64-gnu@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz#837f91a740eb4420c06f34c4677645315479d9be" - integrity sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw== +"@next/swc-linux-arm64-gnu@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz#d8337e2f4881a80221a1f56ac3f979b665b7e574" + integrity sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ== "@next/swc-linux-arm64-musl@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.32.tgz#327efdffe97e56f5389a7889cdedbd676fdbb519" integrity sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ== -"@next/swc-linux-arm64-musl@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz#dc8903469e5c887b25e3c2217a048bd30c58d3d4" - integrity sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg== +"@next/swc-linux-arm64-musl@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz#6c24392824406a50f27fb9cf4e49362d4666db1c" + integrity sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg== "@next/swc-linux-x64-gnu@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.32.tgz#a3e7444613d0fe5c8ea4ead08d6a9c818246758c" integrity sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw== -"@next/swc-linux-x64-gnu@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz#344438be592b6b28cc540194274561e41f9933e5" - integrity sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg== +"@next/swc-linux-x64-gnu@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz#f1bd19fc7d119f27c4cf7f51915aef0f119c943f" + integrity sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ== "@next/swc-linux-x64-musl@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.32.tgz#a2ec5b0a06c740d6740c938b1d4a614f1a13f018" integrity sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg== -"@next/swc-linux-x64-musl@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz#3379fad5e0181000b2a4fac0b80f7ca4ffe795c8" - integrity sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA== +"@next/swc-linux-x64-musl@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz#20c1ea0c98988c337614ce6fda01b82300ff00fd" + integrity sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA== "@next/swc-win32-arm64-msvc@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.32.tgz#b4d3e47c6b276fc4711deb978d04015d029d198d" integrity sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg== -"@next/swc-win32-arm64-msvc@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz#bca8f4dde34656aef8e99f1e5696de255c2f00e5" - integrity sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ== +"@next/swc-win32-arm64-msvc@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz#9d66be9dc4ebc458d445a7f6ee804f416d5c2daf" + integrity sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA== "@next/swc-win32-ia32-msvc@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.32.tgz#d1f1f854a1fbbaeefa8f81271437448653f33494" integrity sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA== -"@next/swc-win32-ia32-msvc@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz#a69c581483ea51dd3b8907ce33bb101fe07ec1df" - integrity sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q== - "@next/swc-win32-x64-msvc@14.2.32": version "14.2.32" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.32.tgz#8212d681cf6858a9e3204728f8f2b161000683ed" integrity sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA== -"@next/swc-win32-x64-msvc@14.2.33": - version "14.2.33" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz#f1a40062530c17c35a86d8c430b3ae465eb7cea1" - integrity sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg== +"@next/swc-win32-x64-msvc@16.1.1": + version "16.1.1" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz#96e9e335e2577481dab6fc7a2af48f3e4a28fbdb" + integrity sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw== "@ngrok/ngrok-android-arm64@1.5.2": version "1.5.2" @@ -2026,7 +2280,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -2817,11 +3071,6 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@rushstack/eslint-patch@^1.3.3": - version "1.12.0" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz#326a7b46f6d4cfa54ae25bb888551697873069b4" - integrity sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw== - "@s2-dev/streamstore@0.17.3": version "0.17.3" resolved "https://registry.yarnpkg.com/@s2-dev/streamstore/-/streamstore-0.17.3.tgz#6db097b2c63850d6fa3dce4fc5be829f596747d3" @@ -3251,6 +3500,13 @@ resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== +"@swc/helpers@0.5.15": + version "0.5.15" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.15.tgz#79efab344c5819ecf83a43f3f9f811fc84b516d7" + integrity sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g== + dependencies: + tslib "^2.8.0" + "@swc/helpers@0.5.5": version "0.5.5" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.5.tgz#12689df71bfc9b21c4f4ca00ae55f2f16c8b77c0" @@ -3417,7 +3673,7 @@ resolved "https://registry.yarnpkg.com/@types/deep-equal/-/deep-equal-1.0.4.tgz#c0a854be62d6b9fae665137a6639aab53389a147" integrity sha512-tqdiS4otQP4KmY0PR3u6KbZ5EWvhNdUoS/jc93UuK23C220lOZ/9TvjfxdPcKvqwwDVtmtSCrnr0p/2dirAxkA== -"@types/estree@*", "@types/estree@^1.0.0": +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== @@ -3470,6 +3726,11 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -3559,7 +3820,7 @@ resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.6.tgz#3c1ab53fd5a23634b8e37ea72ccacbf07fbc7816" integrity sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A== -"@types/prop-types@*", "@types/prop-types@^15.7.12", "@types/prop-types@^15.7.15": +"@types/prop-types@^15.7.12", "@types/prop-types@^15.7.15": version "15.7.15" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== @@ -3571,30 +3832,22 @@ dependencies: "@types/react" "*" -"@types/react-dom@^18": - version "18.3.7" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f" - integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ== +"@types/react-dom@19.2.3": + version "19.2.3" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" + integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== "@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.11": version "4.4.12" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*": - version "19.1.13" - resolved "https://registry.yarnpkg.com/@types/react/-/react-19.1.13.tgz#fc650ffa680d739a25a530f5d7ebe00cdd771883" - integrity sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ== - dependencies: - csstype "^3.0.2" - -"@types/react@^18": - version "18.3.24" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.24.tgz#f6a5a4c613242dfe3af0dcee2b4ec47b92d9b6bd" - integrity sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A== +"@types/react@*", "@types/react@19.2.7": + version "19.2.7" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f" + integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg== dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" + csstype "^3.2.2" "@types/retry@0.12.2": version "0.12.2" @@ -3652,56 +3905,101 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== +"@typescript-eslint/eslint-plugin@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz#8985230730c0d955bf6aa0aed98c5c2c95102e1a" + integrity sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.51.0" + "@typescript-eslint/type-utils" "8.51.0" + "@typescript-eslint/utils" "8.51.0" + "@typescript-eslint/visitor-keys" "8.51.0" + ignore "^7.0.0" + natural-compare "^1.4.0" + ts-api-utils "^2.2.0" + +"@typescript-eslint/parser@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.51.0.tgz#584fb8be3a867cbf980917aabed5f7528f615d6b" + integrity sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A== dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/scope-manager" "8.51.0" + "@typescript-eslint/types" "8.51.0" + "@typescript-eslint/typescript-estree" "8.51.0" + "@typescript-eslint/visitor-keys" "8.51.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== +"@typescript-eslint/project-service@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.51.0.tgz#3cfef313d8bebbf4b2442675a4dd463cef4c8369" + integrity sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/tsconfig-utils" "^8.51.0" + "@typescript-eslint/types" "^8.51.0" + debug "^4.3.4" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== +"@typescript-eslint/scope-manager@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz#19b42f65680c21f7b6f40fe9024327f6bb1893c1" + integrity sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA== + dependencies: + "@typescript-eslint/types" "8.51.0" + "@typescript-eslint/visitor-keys" "8.51.0" -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== +"@typescript-eslint/tsconfig-utils@8.51.0", "@typescript-eslint/tsconfig-utils@^8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz#a575e9885e62dbd260fb64474eff1dae6e317515" + integrity sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw== + +"@typescript-eslint/type-utils@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz#ec165b0312a6025c2a2a3f39641e46ab4f049564" + integrity sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q== dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" + "@typescript-eslint/types" "8.51.0" + "@typescript-eslint/typescript-estree" "8.51.0" + "@typescript-eslint/utils" "8.51.0" debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + ts-api-utils "^2.2.0" + +"@typescript-eslint/types@8.51.0", "@typescript-eslint/types@^8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.51.0.tgz#6996e59d49e92fb893531bdc249f0d92a7bebdbb" + integrity sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag== + +"@typescript-eslint/typescript-estree@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz#b57f5157d1ac2127bd7c2c9ad8060fa017df4a1a" + integrity sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng== + dependencies: + "@typescript-eslint/project-service" "8.51.0" + "@typescript-eslint/tsconfig-utils" "8.51.0" + "@typescript-eslint/types" "8.51.0" + "@typescript-eslint/visitor-keys" "8.51.0" + debug "^4.3.4" + minimatch "^9.0.4" + semver "^7.6.0" + tinyglobby "^0.2.15" + ts-api-utils "^2.2.0" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== +"@typescript-eslint/utils@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.51.0.tgz#b9a071cd210647f860a38873acf9bc5157bea56a" + integrity sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA== dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.51.0" + "@typescript-eslint/types" "8.51.0" + "@typescript-eslint/typescript-estree" "8.51.0" -"@ungap/structured-clone@^1.2.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" - integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== +"@typescript-eslint/visitor-keys@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz#d37f5c82b9bece2c8aeb3ba7bb836bbba0f92bb8" + integrity sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg== + dependencies: + "@typescript-eslint/types" "8.51.0" + eslint-visitor-keys "^4.2.1" "@unrs/resolver-binding-android-arm-eabi@1.11.1": version "1.11.1" @@ -3839,7 +4137,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.1, acorn@^8.9.0: +acorn@^8.14.0, acorn@^8.15.0, acorn@^8.8.1: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -3967,11 +4265,6 @@ array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: is-string "^1.1.1" math-intrinsics "^1.1.0" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" @@ -4674,7 +4967,7 @@ cronstrue@^2.21.0: resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.61.0.tgz#97c79c77045c052afb44cb9f5f8eaf54398094f2" integrity sha512-ootN5bvXbIQI9rW94+QsXN5eROtXWwew6NkdGxIRpS/UFWRggL0G5Al7a9GTBFEsuvVhJ2K3CntIIVt7L2ILhA== -cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: +cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -4754,6 +5047,11 @@ csstype@3.1.3, csstype@^3.0.2, csstype@^3.1.3: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + cva@^1.0.0-beta.3: version "1.0.0-beta.4" resolved "https://registry.yarnpkg.com/cva/-/cva-1.0.0-beta.4.tgz#3feb8b403a1774110eb34e2c409cb0b7c7fbe243" @@ -4959,6 +5257,11 @@ detect-libc@^2.0.0: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.0.tgz#3ca811f60a7b504b0480e5008adacc660b0b8c4f" integrity sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg== +detect-libc@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -4974,13 +5277,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - dlv@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" @@ -5002,13 +5298,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -5350,20 +5639,20 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-next@14.0.4: - version "14.0.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.0.4.tgz#7cd2c0a3b310203d41cf0dbf9d31f9b0a6235b4a" - integrity sha512-9/xbOHEQOmQtqvQ1UsTQZpnA7SlDMBtuKJ//S4JnoyK3oGLhILKXdBgu/UO7lQo/2xOykQULS1qQ6p2+EpHgAQ== +eslint-config-next@16.1.1: + version "16.1.1" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-16.1.1.tgz#536ae0016ac2454e27d625775e0942ff46037116" + integrity sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA== dependencies: - "@next/eslint-plugin-next" "14.0.4" - "@rushstack/eslint-patch" "^1.3.3" - "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" + "@next/eslint-plugin-next" "16.1.1" eslint-import-resolver-node "^0.3.6" eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.28.1" - eslint-plugin-jsx-a11y "^6.7.1" - eslint-plugin-react "^7.33.2" - eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + eslint-plugin-import "^2.32.0" + eslint-plugin-jsx-a11y "^6.10.0" + eslint-plugin-react "^7.37.0" + eslint-plugin-react-hooks "^7.0.0" + globals "16.4.0" + typescript-eslint "^8.46.0" eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: version "0.3.9" @@ -5394,7 +5683,7 @@ eslint-module-utils@^2.12.1: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.28.1: +eslint-plugin-import@^2.32.0: version "2.32.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== @@ -5419,7 +5708,7 @@ eslint-plugin-import@^2.28.1: string.prototype.trimend "^1.0.9" tsconfig-paths "^3.15.0" -eslint-plugin-jsx-a11y@^6.7.1: +eslint-plugin-jsx-a11y@^6.10.0: version "6.10.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== @@ -5440,12 +5729,18 @@ eslint-plugin-jsx-a11y@^6.7.1: safe-regex-test "^1.0.3" string.prototype.includes "^2.0.1" -"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": - version "5.0.0-canary-7118f5dd7-20230705" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz#4d55c50e186f1a2b0636433d2b0b2f592ddbccfd" - integrity sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw== +eslint-plugin-react-hooks@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169" + integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA== + dependencies: + "@babel/core" "^7.24.4" + "@babel/parser" "^7.24.4" + hermes-parser "^0.25.1" + zod "^3.25.0 || ^4.0.0" + zod-validation-error "^3.5.0 || ^4.0.0" -eslint-plugin-react@^7.33.2: +eslint-plugin-react@^7.37.0: version "7.37.5" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== @@ -5469,78 +5764,79 @@ eslint-plugin-react@^7.33.2: string.prototype.matchall "^4.0.12" string.prototype.repeat "^1.0.0" -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8: - version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" - integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.1" - "@humanwhocodes/config-array" "^0.13.0" +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +eslint@^9: + version "9.39.2" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" + integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.21.1" + "@eslint/config-helpers" "^0.4.2" + "@eslint/core" "^0.17.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.39.2" + "@eslint/plugin-kit" "^0.4.1" + "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" ajv "^6.12.4" chalk "^4.0.0" - cross-spawn "^7.0.2" + cross-spawn "^7.0.6" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + esquery "^1.5.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" lodash.merge "^4.6.2" minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== +espree@^10.0.1, espree@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== dependencies: - acorn "^8.9.0" + acorn "^8.15.0" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.2.1" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2: +esquery@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== @@ -5658,7 +5954,18 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9, fast-glob@^3.3.2: +fast-glob@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-glob@^3.3.2: version "3.3.3" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== @@ -5711,12 +6018,12 @@ fetch-retry@^6.0.0: resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-6.0.0.tgz#4ffdf92c834d72ae819e42a4ee2a63f1e9454426" integrity sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag== -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" file-saver@^2.0.5: version "2.0.5" @@ -5751,14 +6058,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + keyv "^4.5.4" flatted@^3.2.9: version "3.3.3" @@ -5941,18 +6247,6 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^10.3.10: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" @@ -5987,12 +6281,15 @@ glob@^9.3.2: minipass "^4.2.4" path-scurry "^1.6.1" -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" +globals@16.4.0: + version "16.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-16.4.0.tgz#574bc7e72993d40cf27cf6c241f324ee77808e51" + integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw== + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globalthis@^1.0.4: version "1.0.4" @@ -6002,18 +6299,6 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - gopd@^1.0.1, gopd@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" @@ -6024,11 +6309,6 @@ graceful-fs@^4.2.11, graceful-fs@^4.2.9: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - has-bigints@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" @@ -6077,6 +6357,18 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hermes-estree@0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480" + integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== + +hermes-parser@^0.25.1: + version "0.25.1" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1" + integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== + dependencies: + hermes-estree "0.25.1" + hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -6167,6 +6459,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +ignore@^7.0.0: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -6415,11 +6712,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -7015,6 +7307,13 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +js-yaml@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + jsdom@^25.0.1: version "25.0.1" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef" @@ -7099,7 +7398,7 @@ json5@^2.2.3: object.assign "^4.1.4" object.values "^1.1.6" -keyv@^4.5.3: +keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -7189,7 +7488,7 @@ long@^5.0.0: resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -7288,7 +7587,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -7333,14 +7632,7 @@ minimal-polyfills@^2.2.3: resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.2.3.tgz#22af58de16807b325f29b83ca38ffb83e75ec3f4" integrity sha512-oxdmJ9cL+xV72h0xYxp4tP2d5/fTBpP45H8DIOn9pASuF8a3IYTf+25fMGDYGiWW+MFsuog6KD6nfmhZJQ+uUw== -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -7457,28 +7749,27 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== -next@14.2.35: - version "14.2.35" - resolved "https://registry.yarnpkg.com/next/-/next-14.2.35.tgz#7c68873a15fe5a19401f2f993fea535be3366ee9" - integrity sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig== +next@16.1.1: + version "16.1.1" + resolved "https://registry.yarnpkg.com/next/-/next-16.1.1.tgz#4cc3477daa0fe22678532ff4778baee95842d866" + integrity sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w== dependencies: - "@next/env" "14.2.35" - "@swc/helpers" "0.5.5" - busboy "1.6.0" + "@next/env" "16.1.1" + "@swc/helpers" "0.5.15" + baseline-browser-mapping "^2.8.3" caniuse-lite "^1.0.30001579" - graceful-fs "^4.2.11" postcss "8.4.31" - styled-jsx "5.1.1" + styled-jsx "5.1.6" optionalDependencies: - "@next/swc-darwin-arm64" "14.2.33" - "@next/swc-darwin-x64" "14.2.33" - "@next/swc-linux-arm64-gnu" "14.2.33" - "@next/swc-linux-arm64-musl" "14.2.33" - "@next/swc-linux-x64-gnu" "14.2.33" - "@next/swc-linux-x64-musl" "14.2.33" - "@next/swc-win32-arm64-msvc" "14.2.33" - "@next/swc-win32-ia32-msvc" "14.2.33" - "@next/swc-win32-x64-msvc" "14.2.33" + "@next/swc-darwin-arm64" "16.1.1" + "@next/swc-darwin-x64" "16.1.1" + "@next/swc-linux-arm64-gnu" "16.1.1" + "@next/swc-linux-arm64-musl" "16.1.1" + "@next/swc-linux-x64-gnu" "16.1.1" + "@next/swc-linux-x64-musl" "16.1.1" + "@next/swc-win32-arm64-msvc" "16.1.1" + "@next/swc-win32-x64-msvc" "16.1.1" + sharp "^0.34.4" next@^14.0.2: version "14.2.32" @@ -8267,13 +8558,12 @@ react-dnd@^16.0.1: fast-deep-equal "^3.1.3" hoist-non-react-statics "^3.3.2" -react-dom@^18.3: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== +react-dom@19.2.3: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" + integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" + scheduler "^0.27.0" react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" @@ -8342,12 +8632,10 @@ react-zoom-pan-pinch@^3.7.0: resolved "https://registry.yarnpkg.com/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz#7db4d2ec49c316eb20f71c56e9012eeb3ef4b504" integrity sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA== -react@^18: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" +react@19.2.3: + version "19.2.3" + resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8" + integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== read-cache@^1.0.0: version "1.0.0" @@ -8549,13 +8837,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rollup@3.29.5: version "3.29.5" resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.5.tgz#8a2e477a758b520fb78daf04bca4c522c1da8a54" @@ -8637,12 +8918,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== semver@^6.3.0, semver@^6.3.1: version "6.3.1" @@ -8654,6 +8933,11 @@ semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.7.1: resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== +semver@^7.6.0, semver@^7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + server-only@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" @@ -8695,6 +8979,40 @@ shallowequal@1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== +sharp@^0.34.4: + version "0.34.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.34.5.tgz#b6f148e4b8c61f1797bde11a9d1cfebbae2c57b0" + integrity sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg== + dependencies: + "@img/colour" "^1.0.0" + detect-libc "^2.1.2" + semver "^7.7.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.34.5" + "@img/sharp-darwin-x64" "0.34.5" + "@img/sharp-libvips-darwin-arm64" "1.2.4" + "@img/sharp-libvips-darwin-x64" "1.2.4" + "@img/sharp-libvips-linux-arm" "1.2.4" + "@img/sharp-libvips-linux-arm64" "1.2.4" + "@img/sharp-libvips-linux-ppc64" "1.2.4" + "@img/sharp-libvips-linux-riscv64" "1.2.4" + "@img/sharp-libvips-linux-s390x" "1.2.4" + "@img/sharp-libvips-linux-x64" "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.2.4" + "@img/sharp-libvips-linuxmusl-x64" "1.2.4" + "@img/sharp-linux-arm" "0.34.5" + "@img/sharp-linux-arm64" "0.34.5" + "@img/sharp-linux-ppc64" "0.34.5" + "@img/sharp-linux-riscv64" "0.34.5" + "@img/sharp-linux-s390x" "0.34.5" + "@img/sharp-linux-x64" "0.34.5" + "@img/sharp-linuxmusl-arm64" "0.34.5" + "@img/sharp-linuxmusl-x64" "0.34.5" + "@img/sharp-wasm32" "0.34.5" + "@img/sharp-win32-arm64" "0.34.5" + "@img/sharp-win32-ia32" "0.34.5" + "@img/sharp-win32-x64" "0.34.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -9093,6 +9411,13 @@ styled-jsx@5.1.1: dependencies: client-only "0.0.1" +styled-jsx@5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.6.tgz#83b90c077e6c6a80f7f5e8781d0f311b2fe41499" + integrity sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== + dependencies: + client-only "0.0.1" + stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" @@ -9223,10 +9548,10 @@ tailwindcss@^3.3.0: resolve "^1.22.8" sucrase "^3.35.0" -tapwrite@1.1.93: - version "1.1.93" - resolved "https://registry.yarnpkg.com/tapwrite/-/tapwrite-1.1.93.tgz#ea7a8d7dd2272a8651dd5b1260dcddebbf3ece26" - integrity sha512-yrXN2I8egnQKgGUNP7Kp7aFZUTZrT0I14Pkxpe+hWNzA17RC1zNcahUzX8hcBf9ytZNRankxk4Q0bjUXW+X9kw== +tapwrite@1.1.94: + version "1.1.94" + resolved "https://registry.yarnpkg.com/tapwrite/-/tapwrite-1.1.94.tgz#e0f72d262139f5679b7c6a142456756ce6db4aa2" + integrity sha512-KtGbpKq6Ya47s7WPi6DN0uYQZCgGGpWYeyhwzbhkL2LlrsumUnLWjK5+o3OH8truHwZNJmyxlsDWY26iLAncXg== dependencies: re-resizable "^6.10.0" tippy.js "^6.3.7" @@ -9314,7 +9639,7 @@ tinyexec@^1.0.1: resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-1.0.2.tgz#bdd2737fe2ba40bd6f918ae26642f264b99ca251" integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg== -tinyglobby@^0.2.13, tinyglobby@^0.2.2: +tinyglobby@^0.2.13, tinyglobby@^0.2.15, tinyglobby@^0.2.2: version "0.2.15" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== @@ -9377,10 +9702,10 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -ts-api-utils@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" - integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== +ts-api-utils@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.3.0.tgz#9f397ac9d88ac76e8dd6e8bc4af0dbf98af99f73" + integrity sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg== ts-interface-checker@^0.1.9: version "0.1.13" @@ -9412,7 +9737,7 @@ tslib@2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tslib@^2.0.3, tslib@^2.4.0: +tslib@^2.0.3, tslib@^2.4.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -9446,11 +9771,6 @@ type-detect@4.0.8: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" @@ -9506,6 +9826,16 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" +typescript-eslint@^8.46.0: + version "8.51.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.51.0.tgz#3cf2ead3d7c5adf940fbac213c043370c0c000b2" + integrity sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA== + dependencies: + "@typescript-eslint/eslint-plugin" "8.51.0" + "@typescript-eslint/parser" "8.51.0" + "@typescript-eslint/typescript-estree" "8.51.0" + "@typescript-eslint/utils" "8.51.0" + typescript@^5: version "5.9.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" @@ -9978,7 +10308,17 @@ zod-validation-error@^1.5.0: resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-1.5.0.tgz#2b355007a1c3b7fb04fa476bfad4e7b3fd5491e3" integrity sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw== +"zod-validation-error@^3.5.0 || ^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918" + integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== + zod@3.25.76, zod@^3.20.2, zod@^3.23.8: version "3.25.76" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +"zod@^3.25.0 || ^4.0.0": + version "4.2.1" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.2.1.tgz#07f0388c7edbfd5f5a2466181cb4adf5b5dbd57b" + integrity sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==