diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 564cff9..03d9549 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,10 +1,6 @@ \ No newline at end of file diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..162eada --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,29 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "languages": { + "JavaScript": { + "format_on_save": "on", + "formatter": ["prettier"], + "code_actions_on_format": { + "source.fixAll.eslint": true + } + }, + "TypeScript": { + "format_on_save": "on", + "formatter": ["prettier"], + "code_actions_on_format": { + "source.fixAll.eslint": true + } + }, + "TSX": { + "format_on_save": "on", + "formatter": ["prettier"], + "code_actions_on_format": { + "source.fixAll.eslint": true + } + } + } +} diff --git a/nx.json b/nx.json index 62c5f29..5b63bdd 100644 --- a/nx.json +++ b/nx.json @@ -23,8 +23,8 @@ "plugins": [ { "plugin": "@nx/vite/plugin", + "buildTargetName": "vite:build", "options": { - "buildTargetName": "vite:build", "testTargetName": "test", "serveTargetName": "serve", "devTargetName": "vite:dev", diff --git a/package.json b/package.json index d9bf03c..6d760c4 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,16 @@ "compile": "yarn build" }, "devDependencies": { - "@nx/eslint": "20.4.1", - "@nx/vite": "20.4.1", - "@nx/web": "20.4.1", - "@vitest/ui": "^1.3.1", - "nx": "20.4.1", - "prettier": "^3.4.2", - "typescript": "^5.7.3", - "vite": "^5.0.0", - "vitest": "^1.3.1" + "@nx/eslint": "^22.0.4", + "@nx/vite": "^22.0.4", + "@nx/web": "^22.0.4", + "@vitest/ui": "^4.0.10", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-no-relative-import-paths": "^1.6.1", + "nx": "^22.0.4", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "vite": "^7.2.2", + "vitest": "^4.0.10" } } diff --git a/packages/executor/Dockerfile b/packages/executor/Dockerfile index 02faa3b..05289b4 100644 --- a/packages/executor/Dockerfile +++ b/packages/executor/Dockerfile @@ -13,7 +13,7 @@ COPY packages/storage ./packages/storage COPY packages/executor ./packages/executor ENV NX_DAEMON=false -RUN yarn nx run executor:build +RUN yarn nx run build extension-a11y-checker-executor FROM --platform=amd64 node:22-slim diff --git a/packages/executor/package.json b/packages/executor/package.json index 4904eaa..c66f66f 100644 --- a/packages/executor/package.json +++ b/packages/executor/package.json @@ -16,22 +16,22 @@ "format:check": "prettier $@ '**/*.{ts,tsx,yaml,yml,json,md,mdx}' --check" }, "dependencies": { - "extension-a11y-checker-storage": "workspace:*", - "lighthouse": "^12.3.0", - "node-cron": "^3.0.3", - "pa11y": "^8.0.0", - "pino": "^9.6.0", - "pino-pretty": "^13.0.0", - "puppeteer": "^24.2.1", + "extension-a11y-checker-storage": "workspace:^", + "lighthouse": "^13.0.1", + "node-cron": "^4.2.1", + "pa11y": "^9.0.1", + "pino": "^10.1.0", + "pino-pretty": "^13.1.2", + "puppeteer": "^24.30.0", "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", + "@eslint/eslintrc": "^3.3.1", + "@types/node": "^24.10.1", "@types/node-cron": "^3.0.11", - "eslint": "^9", - "nodemon": "^3.1.9", - "typescript": "^5" + "eslint": "^9.39.1", + "nodemon": "^3.1.11", + "typescript": "^5.9.3" }, "packageManager": "yarn@4.6.0" } diff --git a/packages/storage/package.json b/packages/storage/package.json index 7b46c9c..0381079 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -26,16 +26,16 @@ "format:check": "prettier $@ '**/*.{ts,tsx,yaml,yml,json,md,mdx}' --check" }, "dependencies": { - "@typegoose/typegoose": "^12.11.0", - "cron-parser": "^4.9.0", - "mongodb": "^6.12.0", - "mongoose": "^8.10.0" + "@typegoose/typegoose": "^12.20.0", + "cron-parser": "^5.4.0", + "mongodb": "^7.0.0", + "mongoose": "^8.20.0" }, "devDependencies": { - "@eslint/eslintrc": "^3", - "@types/node": "^20", - "eslint": "^9", - "typescript": "^5" + "@eslint/eslintrc": "^3.3.1", + "@types/node": "^24.10.1", + "eslint": "^9.39.1", + "typescript": "^5.9.3" }, "packageManager": "yarn@4.6.0" } diff --git a/packages/storage/src/context/context.model.ts b/packages/storage/src/context/context.model.ts index 704f5ab..63f935f 100644 --- a/packages/storage/src/context/context.model.ts +++ b/packages/storage/src/context/context.model.ts @@ -1,12 +1,16 @@ import { prop, modelOptions } from "@typegoose/typegoose"; -import { ObjectId } from "mongoose"; -import { getModel } from "../lib/mongoose.js"; +import { getModel, serializeObjectWithIds } from "../lib/mongoose.js"; import { ReturnModelType } from "@typegoose/typegoose/lib/types"; import { ScanProfileModel } from "../scanProfile/scanProfile.model.js"; -import { ScanModel } from "../scan/scan.model.js"; @modelOptions({ - schemaOptions: { versionKey: false, collection: "contexts" }, + schemaOptions: { + versionKey: false, + collection: "contexts", + toJSON: { + transform: (doc, ret) => serializeObjectWithIds(ret), + }, + }, options: { automaticName: false }, }) export class Context { @@ -29,7 +33,7 @@ export class Context { this: ReturnModelType, contextId: string, ) { - return this.updateOne( + return this.findOneAndUpdate( { _id: contextId }, { $setOnInsert: { _id: contextId }, diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 516903e..75b6522 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -4,3 +4,4 @@ export { dbConnect }; export * from "./context/context.model.js"; export * from "./scan/scan.model.js"; export * from "./scanProfile/scanProfile.model.js"; +export { serializeObjectWithIds } from "./lib/mongoose.js"; diff --git a/packages/storage/src/lib/mongoose.ts b/packages/storage/src/lib/mongoose.ts index 3fc881f..ec5e758 100644 --- a/packages/storage/src/lib/mongoose.ts +++ b/packages/storage/src/lib/mongoose.ts @@ -1,4 +1,4 @@ -import mongoose from "mongoose"; +import mongoose, { ObjectId } from "mongoose"; import type { AnyParamConstructor, ReturnModelType, @@ -14,3 +14,49 @@ export function getModel>( getModelForClass(modelClass, { options: { customName: modelName } }) ); } + +export type Serialize = T extends ObjectId + ? string + : T extends (infer U)[] + ? Serialize[] + : T extends object + ? { [K in keyof T]: Serialize } + : T; + +function isObjectId(value: unknown): value is ObjectId { + return ( + !!value && + typeof value === "object" && + ((value as any).toHexString instanceof Function || + ((value as any).toString instanceof Function && + (value as any).constructor?.name === "ObjectId")) + ); +} + +function isPlainObject(value: unknown): value is Record { + if (Object.prototype.toString.call(value) !== "[object Object]") return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +export function serializeObjectWithIds(obj: T): Serialize { + if (obj == null) return obj as Serialize; + + if (isObjectId(obj)) return obj.toString() as Serialize; + + if (Array.isArray(obj)) { + return (obj as unknown[]).map(serializeObjectWithIds) as Serialize; + } + + if (isPlainObject(obj)) { + const result: Record = {}; + for (const key of Object.keys(obj as Record)) { + result[key] = serializeObjectWithIds( + (obj as Record)[key], + ); + } + return result as Serialize; + } + + return obj as Serialize; +} diff --git a/packages/storage/src/scan/scan.model.ts b/packages/storage/src/scan/scan.model.ts index 9972530..1e37802 100644 --- a/packages/storage/src/scan/scan.model.ts +++ b/packages/storage/src/scan/scan.model.ts @@ -3,9 +3,16 @@ import { isDocument } from "@typegoose/typegoose"; import { index, modelOptions, prop } from "@typegoose/typegoose"; import { ObjectId } from "mongodb"; import { ScanProfile } from "../scanProfile/scanProfile.model.js"; -import { getModel } from "../lib/mongoose.js"; +import { getModel, serializeObjectWithIds } from "../lib/mongoose.js"; -@modelOptions({ schemaOptions: { _id: false } }) +@modelOptions({ + schemaOptions: { + _id: false, + toJSON: { + transform: (doc, ret) => serializeObjectWithIds(ret), + }, + }, +}) export class Issue { @prop({ required: true }) public url: string; @@ -27,7 +34,13 @@ export class Issue { } @modelOptions({ - schemaOptions: { _id: false, suppressReservedKeysWarning: true }, + schemaOptions: { + _id: false, + suppressReservedKeysWarning: true, + toJSON: { + transform: (doc, ret) => serializeObjectWithIds(ret), + }, + }, }) export class Issues { @prop({ required: true }) @@ -38,7 +51,14 @@ export class Issues { public notices: number = 0; } -@modelOptions({ schemaOptions: { _id: false } }) +@modelOptions({ + schemaOptions: { + _id: false, + toJSON: { + transform: (doc, ret) => serializeObjectWithIds(ret), + }, + }, +}) export class Page { @prop({ required: true }) public url: string; @@ -56,7 +76,13 @@ export class Page { @index({ status: 1, executionScheduledFor: 1 }) @modelOptions({ - schemaOptions: { collection: "scans", versionKey: false }, + schemaOptions: { + collection: "scans", + versionKey: false, + toJSON: { + transform: (doc, ret) => serializeObjectWithIds(ret), + }, + }, options: { automaticName: false }, }) export class Scan { diff --git a/packages/storage/src/scanProfile/scanProfile.model.ts b/packages/storage/src/scanProfile/scanProfile.model.ts index dde2642..dbc6df9 100644 --- a/packages/storage/src/scanProfile/scanProfile.model.ts +++ b/packages/storage/src/scanProfile/scanProfile.model.ts @@ -1,9 +1,9 @@ import type { DocumentType, Ref } from "@typegoose/typegoose"; import { modelOptions, prop } from "@typegoose/typegoose"; -import cronParser from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; import { ObjectId } from "mongodb"; import { Context, ContextModel } from "../context/context.model.js"; -import { getModel } from "../lib/mongoose.js"; +import { getModel, serializeObjectWithIds } from "../lib/mongoose.js"; import { ReturnModelType } from "@typegoose/typegoose/lib/types"; import { Scan, ScanModel } from "../scan/scan.model.js"; @@ -17,7 +17,10 @@ class CronSchedule { schemaOptions: { collection: "scanprofiles", versionKey: false, - toJSON: { virtuals: true }, + toJSON: { + virtuals: true, + transform: (doc, ret) => serializeObjectWithIds(ret), + }, toObject: { virtuals: true }, }, options: { automaticName: false }, @@ -79,7 +82,7 @@ export class ScanProfile { match: { completedAt: { $exists: true } }, options: { sort: { completedAt: -1 } }, }) - public lastScan: Scan | null = null; + public lastScan: Ref; @prop({ ref: () => "Scan", @@ -89,7 +92,7 @@ export class ScanProfile { match: { status: { $in: ["queued", "running"] } }, options: { sort: { executionScheduledFor: 1 } }, }) - public nextScan: Scan | null = null; + public nextScan: Ref; public static async delete( this: ReturnModelType, @@ -107,14 +110,10 @@ export class ScanProfile { if (!context) { return null; } - const profiles = await ScanProfileModel.find({ context: contextId }).exec(); - await Promise.all( - profiles.map(async (p) => { - await p.populate("nextScan"); - await p.populate({ path: "lastScan", select: "-issues" }); - }), - ); - return profiles; + return await ScanProfileModel.find({ context: contextId }) + .populate("nextScan") + .populate({ path: "lastScan", select: "-issues" }) + .exec(); } public nextExecution(this: DocumentType) { @@ -122,8 +121,7 @@ export class ScanProfile { return null; } - return cronParser - .parseExpression(this.cronSchedule.expression) + return CronExpressionParser.parse(this.cronSchedule.expression) .next() .toDate(); } diff --git a/packages/web2/Dockerfile b/packages/web2/Dockerfile index b017faf..889ca3f 100644 --- a/packages/web2/Dockerfile +++ b/packages/web2/Dockerfile @@ -11,7 +11,7 @@ COPY packages/storage ./packages/storage RUN corepack enable && yarn install ENV NX_DAEMON=false -RUN yarn nx run web2:build +RUN yarn nx run build extension-a11y-checker-web2 FROM node:22-slim diff --git a/packages/web2/app.config.ts b/packages/web2/app.config.ts deleted file mode 100644 index 3fe6d31..0000000 --- a/packages/web2/app.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { defineConfig } from "@tanstack/react-start/config"; -import tsConfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - server: { - esbuild: { - options: { - minify: false, - target: "es2022", - }, - }, - }, - vite: { - server: { - allowedHosts: ["host.docker.internal"], - }, - build: { - minify: false, - }, - resolve: { - alias: { - // /esm/icons/index.mjs only exports the icons statically, so no separate chunks are created - "@tabler/icons-react": "@tabler/icons-react/dist/esm/icons/index.mjs", - }, - }, - plugins: [ - tsConfigPaths({ - projects: ["./tsconfig.json"], - }), - ], - }, -}); diff --git a/packages/web2/app/actions/commons.ts b/packages/web2/app/actions/commons.ts index a623bdd..c8244c4 100644 --- a/packages/web2/app/actions/commons.ts +++ b/packages/web2/app/actions/commons.ts @@ -1,6 +1,6 @@ import { ScanModel, ScanProfileModel } from "extension-a11y-checker-storage"; import { notFound } from "@tanstack/react-router"; -import cronParser from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; export async function assertProfile(profileId: string, contextId?: string) { const profile = await ScanProfileModel.findById(profileId); @@ -23,7 +23,7 @@ export async function scheduleScan( export const validateCron = (cronExpression: string | undefined) => { if (cronExpression) { try { - const interval = cronParser.parseExpression(cronExpression); + const interval = CronExpressionParser.parse(cronExpression); const firstDate = interval.next(); const secondDate = interval.next(); const hoursDiff = diff --git a/packages/web2/app/actions/middleware.ts b/packages/web2/app/actions/middleware.ts index 766b730..c2284e4 100644 --- a/packages/web2/app/actions/middleware.ts +++ b/packages/web2/app/actions/middleware.ts @@ -2,7 +2,7 @@ import { createMiddleware } from "@tanstack/react-start"; import { notFound } from "@tanstack/react-router"; import { dbConnect, ScanProfileModel } from "extension-a11y-checker-storage"; import { z } from "zod"; -import { getHeader } from "@tanstack/react-start/server"; +import { getRequestHeader } from "@tanstack/react-start/server"; import { getAccessToken, verify } from "@mittwald/ext-bridge/node"; import { getSessionToken } from "@mittwald/ext-bridge/browser"; @@ -18,7 +18,7 @@ const getToken = async (sessionToken: string) => { return (await getAccessToken(sessionToken, extensionSecret)).publicToken; }; -export const authenticateMiddleware = createMiddleware({ validateClient: true }) +export const authenticateMiddleware = createMiddleware({ type: "function" }) .client(async ({ next }) => { const token = await getSessionToken(); return next({ @@ -26,7 +26,7 @@ export const authenticateMiddleware = createMiddleware({ validateClient: true }) }); }) .server(async ({ next }) => { - const sessionToken = getHeader("x-session-token"); + const sessionToken = getRequestHeader("x-session-token"); const verifiedToken = await verify(sessionToken!); const apiToken = await getToken(sessionToken!); @@ -45,9 +45,9 @@ const contextSchema = z }) .catchall(z.any()); -export const contextMatchingMiddleware = createMiddleware() +export const contextMatchingMiddleware = createMiddleware({ type: "function" }) .middleware([authenticateMiddleware]) - .validator(contextSchema) + .inputValidator(contextSchema) .server(async ({ next, context, data: { contextId } }) => { if (context.contextId !== contextId) { throw notFound(); @@ -73,18 +73,20 @@ async function assertContextMatching(profileId: string, contextId: string) { } } -export const profileIdAuthorizeMiddleware = createMiddleware() +export const profileIdAuthorizeMiddleware = createMiddleware({ + type: "function", +}) .middleware([dbMiddleware, authenticateMiddleware]) - .validator(profileIdSchema) + .inputValidator(profileIdSchema) .server(async ({ next, context, data: profileId }) => { const { contextId } = context; await assertContextMatching(profileId, contextId); return next(); }); -export const profileAuthorizeMiddleware = createMiddleware() +export const profileAuthorizeMiddleware = createMiddleware({ type: "function" }) .middleware([dbMiddleware, authenticateMiddleware]) - .validator(profileSchema) + .inputValidator(profileSchema) .server(async ({ next, context, data: { profileId } }) => { const { contextId } = context; await assertContextMatching(profileId, contextId); diff --git a/packages/web2/app/actions/profile.ts b/packages/web2/app/actions/profile.ts index 0249cc5..bc8a9aa 100644 --- a/packages/web2/app/actions/profile.ts +++ b/packages/web2/app/actions/profile.ts @@ -1,7 +1,11 @@ import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; -import { ScanModel, ScanProfileModel } from "extension-a11y-checker-storage"; -import { Scan, ScanProfile } from "../api/types.ts"; +import { + ScanModel, + ScanProfileModel, + serializeObjectWithIds, +} from "extension-a11y-checker-storage"; +import { Scan, ScanProfile } from "~/api/types.ts"; import { ObjectId } from "mongodb"; import { notFound } from "@tanstack/react-router"; import { @@ -12,10 +16,11 @@ import { profileIdAuthorizeMiddleware, } from "./middleware.js"; import { scheduleScan, validateCron } from "./commons.js"; +import { isDocument } from "@typegoose/typegoose"; export const getProfiles = createServerFn() .middleware([dbMiddleware, authenticateMiddleware]) - .validator(z.string()) + .inputValidator(z.string()) .handler(async ({ data: contextId }) => { const data = await ScanProfileModel.findForContext(contextId); if (data === null) { @@ -23,10 +28,15 @@ export const getProfiles = createServerFn() } const profiles = data.map((profileDoc) => { - const profileObject = profileDoc.toObject(); + const profileObject = profileDoc.toJSON(); + + const issueSummary = isDocument(profileDoc.lastScan) + ? profileDoc.lastScan.getIssueSummary() + : undefined; + return { ...profileObject, - issueSummary: profileDoc.lastScan?.getIssueSummary(), + issueSummary, } as unknown as ScanProfile; }); return profiles; @@ -36,7 +46,7 @@ export const getProfile = createServerFn({ method: "GET", }) .middleware([dbMiddleware, profileIdAuthorizeMiddleware]) - .validator(z.string()) + .inputValidator(z.string()) .handler(async ({ data: profileId }) => { const profile = await ScanProfileModel.findById(profileId).exec(); await profile?.populate("nextScan"); @@ -46,18 +56,19 @@ export const getProfile = createServerFn({ return { profile: { - ...profile?.toObject(), + ...profile?.toJSON(), issueSummary: lastScan?.getIssueSummary(), } as unknown as ScanProfile, - - lastScan: lastScan as unknown as Scan | undefined, - lastSuccessfulScan: lastSuccessfulScan as unknown as Scan | undefined, + lastScan: serializeObjectWithIds(lastScan?.toJSON()) as unknown as Scan, + lastSuccessfulScan: serializeObjectWithIds( + lastSuccessfulScan?.toJSON(), + ) as unknown as Scan, }; }); export const createProfile = createServerFn({ method: "POST" }) .middleware([dbMiddleware, contextMatchingMiddleware]) - .validator( + .inputValidator( z.object({ contextId: z.string(), domain: z.string(), @@ -77,7 +88,7 @@ export const createProfile = createServerFn({ method: "POST" }) export const updateProfilePaths = createServerFn({ method: "POST" }) .middleware([dbMiddleware, profileAuthorizeMiddleware]) - .validator( + .inputValidator( z.object({ profileId: z.string(), paths: z.array(z.string()), @@ -90,14 +101,14 @@ export const updateProfilePaths = createServerFn({ method: "POST" }) { new: true }, ); if (!profile) { - return new Response("Profile not found", { status: 404 }); + throw notFound({ data: "Profile not found" }); } return profile.toJSON() as unknown as ScanProfile; }); export const updateProfileName = createServerFn({ method: "POST" }) .middleware([dbMiddleware, profileAuthorizeMiddleware]) - .validator( + .inputValidator( z.object({ profileId: z.string(), name: z.string(), @@ -110,14 +121,14 @@ export const updateProfileName = createServerFn({ method: "POST" }) { new: true }, ); if (!profile) { - return new Response("Profile not found", { status: 404 }); + throw notFound({ data: "Profile not found" }); } return profile.toJSON() as unknown as ScanProfile; }); export const updateProfileDomain = createServerFn({ method: "POST" }) .middleware([dbMiddleware, profileAuthorizeMiddleware]) - .validator( + .inputValidator( z.object({ profileId: z.string(), updateName: z.boolean().optional(), @@ -137,7 +148,7 @@ export const updateProfileDomain = createServerFn({ method: "POST" }) { new: true }, ); if (!profile) { - return new Response("Profile not found", { status: 404 }); + throw notFound({ data: "Profile not found" }); } const nextScan = await ScanModel.nextScanOfProfile(profileId); @@ -150,7 +161,7 @@ export const updateProfileDomain = createServerFn({ method: "POST" }) export const updateProfileCron = createServerFn({ method: "POST" }) .middleware([dbMiddleware, profileAuthorizeMiddleware]) - .validator( + .inputValidator( z.object({ profileId: z.string(), cronExpression: z.string(), @@ -159,7 +170,7 @@ export const updateProfileCron = createServerFn({ method: "POST" }) .handler(async ({ data: { profileId, cronExpression } }) => { const validationResult = validateCron(cronExpression); if (validationResult !== true) { - return validationResult; + throw validationResult; } const cronUpdateSet = cronExpression @@ -176,7 +187,7 @@ export const updateProfileCron = createServerFn({ method: "POST" }) { new: true }, ); if (!profile) { - return new Response("Profile not found", { status: 404 }); + throw notFound({ data: "Profile not found" }); } await ScanModel.deleteScheduledForProfile(profile); @@ -187,7 +198,7 @@ export const updateProfileCron = createServerFn({ method: "POST" }) export const updateProfileSettings = createServerFn({ method: "POST" }) .middleware([dbMiddleware, profileAuthorizeMiddleware]) - .validator( + .inputValidator( z.object({ profileId: z.string(), cronExpression: z.string().optional(), @@ -208,7 +219,7 @@ export const updateProfileSettings = createServerFn({ method: "POST" }) }) => { const validationResult = validateCron(cronExpression); if (validationResult !== true) { - return validationResult; + throw validationResult; } const cronUpdateSet = cronExpression @@ -229,7 +240,7 @@ export const updateProfileSettings = createServerFn({ method: "POST" }) { new: true }, ); if (!profile) { - return new Response("Profile not found", { status: 404 }); + throw notFound({ data: "Profile not found" }); } return profile.toJSON() as unknown as ScanProfile; }, @@ -237,7 +248,7 @@ export const updateProfileSettings = createServerFn({ method: "POST" }) export const deleteProfile = createServerFn({ method: "POST" }) .middleware([profileIdAuthorizeMiddleware]) - .validator(z.string()) + .inputValidator(z.string()) .handler(async ({ data: profileId }) => { await ScanProfileModel.delete(profileId); }); diff --git a/packages/web2/app/actions/scan.ts b/packages/web2/app/actions/scan.ts index a58282e..bd0cedb 100644 --- a/packages/web2/app/actions/scan.ts +++ b/packages/web2/app/actions/scan.ts @@ -5,7 +5,7 @@ import { dbMiddleware, profileAuthorizeMiddleware } from "./middleware.js"; export const startScan = createServerFn({ method: "POST" }) .middleware([dbMiddleware, profileAuthorizeMiddleware]) - .validator( + .inputValidator( z.object({ profileId: z.string(), isSystemScan: z.boolean().optional() }), ) .handler(async ({ data: { profileId, isSystemScan } }) => { diff --git a/packages/web2/app/api.ts b/packages/web2/app/api.ts deleted file mode 100644 index b8cc194..0000000 --- a/packages/web2/app/api.ts +++ /dev/null @@ -1,10 +0,0 @@ -// app/api.ts -import { - createStartAPIHandler, - defaultAPIFileRouteHandler, -} from "@tanstack/react-start/api"; -import { dbConnect } from "extension-a11y-checker-storage"; - -await dbConnect(); - -export default createStartAPIHandler(defaultAPIFileRouteHandler); diff --git a/packages/web2/app/api/helpers.ts b/packages/web2/app/api/helpers.ts index 17f9e41..f8b0e73 100644 --- a/packages/web2/app/api/helpers.ts +++ b/packages/web2/app/api/helpers.ts @@ -1,7 +1,12 @@ -import { logger } from "../logger.js"; +import { logger } from "~/logger.js"; import { isNotFound } from "@tanstack/react-router"; import { json } from "@tanstack/react-start"; -import { SafeParseReturnType, SafeParseSuccess, ZodType } from "zod"; +import { + treeifyError, + ZodSafeParseResult, + ZodSafeParseSuccess, + ZodType, +} from "zod"; export const handleAPIError = (e: unknown, action?: string) => { if (e instanceof Response) { @@ -15,22 +20,20 @@ export const handleAPIError = (e: unknown, action?: string) => { return json({ message: "Internal Server Error", action }, { status: 500 }); }; -export const assertValidationSuccess: ( - parseResult: SafeParseReturnType, -) => asserts parseResult is SafeParseSuccess = ( - parseResult: SafeParseReturnType, -): asserts parseResult is SafeParseSuccess => { +export function assertValidationSuccess( + parseResult: ZodSafeParseResult, +): asserts parseResult is ZodSafeParseSuccess { if (!parseResult.success) { logger.debug(parseResult.error); throw json( { message: "Input validation failed", - error: { ...parseResult.error, name: "ValidationError" }, + error: { ...treeifyError(parseResult.error), name: "ValidationError" }, }, { status: 400 }, ); } -}; +} /* * Due to a missing typescript feature, the return type of this function can not be @@ -42,8 +45,8 @@ export const assertValidationSuccess: ( * // ^ this will ensure `parsedInput` gets the type information * ``` */ -export const assertValidation = async ( - schema: ZodType, +export const assertValidation = async ( + schema: T, value: unknown, ) => { const validationResult = await schema.safeParseAsync(value); diff --git a/packages/web2/app/client.tsx b/packages/web2/app/client.tsx deleted file mode 100644 index d5a3e71..0000000 --- a/packages/web2/app/client.tsx +++ /dev/null @@ -1,9 +0,0 @@ -// app/client.tsx -/// -import { hydrateRoot } from "react-dom/client"; -import { StartClient } from "@tanstack/react-start"; -import { createRouter } from "./router"; - -const router = createRouter(); - -hydrateRoot(document, ); diff --git a/packages/web2/app/components/create/components/DomainSelect.tsx b/packages/web2/app/components/create/components/DomainSelect.tsx index d96ba11..1eb0e54 100644 --- a/packages/web2/app/components/create/components/DomainSelect.tsx +++ b/packages/web2/app/components/create/components/DomainSelect.tsx @@ -8,7 +8,7 @@ import { Text, } from "@mittwald/flow-remote-react-components"; import { useServerFn } from "@tanstack/react-start"; -import { getDomains as getDomainsServerFn } from "../../../actions/domain.js"; +import { getDomains as getDomainsServerFn } from "~/actions/domain.js"; import { useQuery } from "@tanstack/react-query"; import { Field } from "@mittwald/flow-remote-react-components/react-hook-form"; diff --git a/packages/web2/app/components/create/components/domain.tsx b/packages/web2/app/components/create/components/domain.tsx index e62d1b5..78d5db2 100644 --- a/packages/web2/app/components/create/components/domain.tsx +++ b/packages/web2/app/components/create/components/domain.tsx @@ -5,7 +5,7 @@ import { TextField, } from "@mittwald/flow-remote-react-components"; import { Field } from "@mittwald/flow-remote-react-components/react-hook-form"; -import { extractDomainFromUrl } from "../helpers.ts"; +import { extractDomainFromUrl } from "~/components/create/helpers.ts"; import { UseFormReturn } from "react-hook-form"; interface DomainFormValues { diff --git a/packages/web2/app/components/create/components/pathsList.tsx b/packages/web2/app/components/create/components/pathsList.tsx index 4a00090..03f945e 100644 --- a/packages/web2/app/components/create/components/pathsList.tsx +++ b/packages/web2/app/components/create/components/pathsList.tsx @@ -1,4 +1,4 @@ -import { UseFormReturn } from "react-hook-form"; +import { UseFormReturn, useWatch } from "react-hook-form"; import { useState } from "react"; import { Align, @@ -12,8 +12,11 @@ import { TextField, typedList, } from "@mittwald/flow-remote-react-components"; -import { FormValues } from "../types.ts"; -import { extractPathFromUrl, prependPathWithSlash } from "../helpers.ts"; +import { FormValues } from "~/components/create/types.ts"; +import { + extractPathFromUrl, + prependPathWithSlash, +} from "~/components/create/helpers.ts"; type PathFormValues = Pick; @@ -29,6 +32,11 @@ export const PathsList = ({ const paths = form.watch("paths"); + useWatch({ + control: form.control, + name: "paths", + }); + const isValidInputValue = () => { if (!pathInputValue.startsWith("/")) { return ( @@ -37,7 +45,7 @@ export const PathsList = ({ ); } - if (paths.has(pathInputValue)) { + if (paths.includes(pathInputValue)) { return "Pfad ist bereits hinzugefügt."; } return true; @@ -49,15 +57,17 @@ export const PathsList = ({ } const values = form.getValues("paths"); - values.add(value); + values.push(value); form.setValue("paths", values); setTouched(false); } const removePathFromFormValues = (value: string) => { const values = form.getValues("paths"); - values.delete(value.toString()); - form.setValue("paths", values); + form.setValue( + "paths", + values.filter((v) => v != value), + ); }; const PathList = typedList(); diff --git a/packages/web2/app/components/create/createModal.tsx b/packages/web2/app/components/create/createModal.tsx index ac94359..d8252bb 100644 --- a/packages/web2/app/components/create/createModal.tsx +++ b/packages/web2/app/components/create/createModal.tsx @@ -12,14 +12,14 @@ import { SegmentedControl, Text, } from "@mittwald/flow-remote-react-components"; -import { useForm } from "react-hook-form"; +import { useForm, UseFormReturn } from "react-hook-form"; import { Form } from "@mittwald/flow-remote-react-components/react-hook-form"; import { FormValues } from "./types.ts"; import { PathsList } from "./components/pathsList.tsx"; import { Domain } from "./components/domain.tsx"; -import { createProfile } from "../../actions/profile.ts"; -import { Route } from "../../routes/index.js"; -import { useGoToProfile } from "../../hooks/useGoTo.js"; +import { createProfile } from "~/actions/profile.ts"; +import { Route } from "~/routes"; +import { useGoToProfile } from "~/hooks/useGoTo.js"; import { DomainSelect } from "./components/DomainSelect.js"; import { useState } from "react"; @@ -31,7 +31,7 @@ export const CreateModal = () => { const form = useForm({ defaultValues: { domain: "", - paths: new Set(["/"]), + paths: ["/"], }, }); @@ -64,14 +64,20 @@ export const CreateModal = () => { setShowCustomDomain(!showCustomDomain)} + onChange={() => setShowCustomDomain((value) => !value)} > mStudio Domain Individuelle Eingabe {!showCustomDomain && } - {showCustomDomain && } + {showCustomDomain && ( + > + } + /> + )}
Unterseiten hinzufügen @@ -79,7 +85,10 @@ export const CreateModal = () => { deiner Website im Blick zu behalten.
- + >} + autoFocus={!!form.getValues("domain")} + /> diff --git a/packages/web2/app/components/create/types.ts b/packages/web2/app/components/create/types.ts index fd5885b..56103d6 100644 --- a/packages/web2/app/components/create/types.ts +++ b/packages/web2/app/components/create/types.ts @@ -1,4 +1,4 @@ export interface FormValues { domain: string; - paths: Set; + paths: string[]; } diff --git a/packages/web2/app/components/errorRoot.tsx b/packages/web2/app/components/errorRoot.tsx index ec61b86..58ccc72 100644 --- a/packages/web2/app/components/errorRoot.tsx +++ b/packages/web2/app/components/errorRoot.tsx @@ -1,5 +1,5 @@ import { ErrorRouteComponent } from "@tanstack/react-router"; -import { RootDocument } from "./rootDocument.tsx"; +import { RootDocument } from "./rootDocument"; export const ErrorRoot: ErrorRouteComponent = ({ error, info }) => { return ( diff --git a/packages/web2/app/components/list/feedbackBox.tsx b/packages/web2/app/components/list/feedbackBox.tsx index 37230ae..190ce2e 100644 --- a/packages/web2/app/components/list/feedbackBox.tsx +++ b/packages/web2/app/components/list/feedbackBox.tsx @@ -9,7 +9,7 @@ import { Text, LayoutCard, } from "@mittwald/flow-remote-react-components"; -import martinImage from "../../feedback-person.webp?inline"; +import martinImage from "~/feedback-person.webp?inline"; export const FeedbackBox = () => { return ( diff --git a/packages/web2/app/components/list/noProfiles.tsx b/packages/web2/app/components/list/noProfiles.tsx index e5f10f2..9b4f594 100644 --- a/packages/web2/app/components/list/noProfiles.tsx +++ b/packages/web2/app/components/list/noProfiles.tsx @@ -8,7 +8,7 @@ import { Text, } from "@mittwald/flow-remote-react-components"; import { IconAccessible } from "@tabler/icons-react"; -import { CreateModal } from "../create/createModal.tsx"; +import { CreateModal } from "~/components/create/createModal.tsx"; export const NoProfiles = () => { return ( diff --git a/packages/web2/app/components/list/profileListContextMenu.tsx b/packages/web2/app/components/list/profileListContextMenu.tsx index 4e9d2bf..3885429 100644 --- a/packages/web2/app/components/list/profileListContextMenu.tsx +++ b/packages/web2/app/components/list/profileListContextMenu.tsx @@ -1,5 +1,5 @@ -import { ScanProfile } from "../../api/types.ts"; -import { useGoToProfile } from "../../hooks/useGoTo.tsx"; +import { ScanProfile } from "~/api/types.ts"; +import { useGoToProfile } from "~/hooks/useGoTo.tsx"; import { useRouter } from "@tanstack/react-router"; import { ContextMenu, @@ -10,11 +10,11 @@ import { MenuItem, useOverlayController, } from "@mittwald/flow-remote-react-components"; -import { startScan } from "../../actions/scan.ts"; +import { startScan } from "~/actions/scan.ts"; import { IconWorldSearch } from "@tabler/icons-react"; -import { RenameProfileModal } from "../profile/modals/renameProfileModal.tsx"; -import { DeleteConfirmationModal } from "../profile/modals/deleteConfirmation.tsx"; -import { isRunningOrPending } from "../profile/helpers.ts"; +import { RenameProfileModal } from "~/components/profile/modals/renameProfileModal.tsx"; +import { DeleteConfirmationModal } from "~/components/profile/modals/deleteConfirmation.tsx"; +import { isRunningOrPending } from "~/components/profile/helpers.ts"; export function ProfileListContextMenu({ profile }: { profile: ScanProfile }) { const goToProfile = useGoToProfile(); diff --git a/packages/web2/app/components/list/profileListItemView.tsx b/packages/web2/app/components/list/profileListItemView.tsx index 1c33bf8..586312e 100644 --- a/packages/web2/app/components/list/profileListItemView.tsx +++ b/packages/web2/app/components/list/profileListItemView.tsx @@ -1,5 +1,5 @@ -import { ScanProfile } from "../../api/types.ts"; -import { isPending, isRunning } from "../profile/helpers.ts"; +import { ScanProfile } from "~/api/types.ts"; +import { isPending, isRunning } from "~/components/profile/helpers.ts"; import { AlertBadge, Avatar, diff --git a/packages/web2/app/components/list/profilesList.tsx b/packages/web2/app/components/list/profilesList.tsx index 3d94b19..8186577 100644 --- a/packages/web2/app/components/list/profilesList.tsx +++ b/packages/web2/app/components/list/profilesList.tsx @@ -1,14 +1,14 @@ -import { ScanProfile } from "../../api/types.ts"; +import { ScanProfile } from "~/api/types.ts"; import { ActionGroup, Flex, Text, typedList, } from "@mittwald/flow-remote-react-components"; -import { isRunningOrPending } from "../profile/helpers.ts"; -import { useAutoRefresh } from "../../hooks/useAutoRefresh.tsx"; -import { useGoToProfile } from "../../hooks/useGoTo.tsx"; -import { CreateProfileButton } from "../create/createProfileButton.tsx"; +import { isRunningOrPending } from "~/components/profile/helpers.ts"; +import { useAutoRefresh } from "~/hooks/useAutoRefresh.tsx"; +import { useGoToProfile } from "~/hooks/useGoTo.tsx"; +import { CreateProfileButton } from "~/components/create/createProfileButton.tsx"; import { ProfileListItemView } from "./profileListItemView.tsx"; export const ProfilesList = ({ profiles }: { profiles: ScanProfile[] }) => { diff --git a/packages/web2/app/components/notFound.tsx b/packages/web2/app/components/notFound.tsx new file mode 100644 index 0000000..db117f2 --- /dev/null +++ b/packages/web2/app/components/notFound.tsx @@ -0,0 +1,11 @@ +import { NotFoundRouteComponent } from "@tanstack/react-router"; +import { LayoutCard, Text } from "@mittwald/flow-remote-react-components"; + +export const NotFound: NotFoundRouteComponent = ({ data }) => { + console.error(data); + return ( + + 404 – Nicht gefunden + + ); +}; diff --git a/packages/web2/app/components/notFoundRoot.tsx b/packages/web2/app/components/notFoundRoot.tsx index 6c8c4ef..cc91c1e 100644 --- a/packages/web2/app/components/notFoundRoot.tsx +++ b/packages/web2/app/components/notFoundRoot.tsx @@ -1,11 +1,11 @@ import { NotFoundRouteComponent } from "@tanstack/react-router"; -import { RootDocument } from "./rootDocument.js"; +import { RootDocument } from "./rootDocument.tsx"; +import { NotFound } from "./notFound.tsx"; -export const NotFoundRoot: NotFoundRouteComponent = ({ data }) => { - console.error(data); +export const NotFoundRoot: NotFoundRouteComponent = (props) => { return ( -

404 – Nicht gefunden

+
); }; diff --git a/packages/web2/app/components/profile/CronFields/lib.ts b/packages/web2/app/components/profile/CronFields/lib.ts index c9e8990..24f4de2 100644 --- a/packages/web2/app/components/profile/CronFields/lib.ts +++ b/packages/web2/app/components/profile/CronFields/lib.ts @@ -1,7 +1,7 @@ import { Time } from "@internationalized/date"; import cronstrue from "cronstrue"; import "cronstrue/locales/de"; -import parser from "cron-parser"; +import { CronExpressionParser } from "cron-parser"; // todo: get actual language export const getCronText = (cronSyntax: string) => { @@ -154,7 +154,7 @@ export const isSmallIntervall = (schedule: string) => { return false; } try { - const interval = parser.parseExpression(schedule); + const interval = CronExpressionParser.parse(schedule); const firstDate = interval.next(); const secondDate = interval.next(); const hoursDiff = @@ -170,7 +170,7 @@ export const isSmallIntervall = (schedule: string) => { export const getExecutions = (cron: string) => { try { - const interval = parser.parseExpression(cron); + const interval = CronExpressionParser.parse(cron); const executions: Date[] = []; diff --git a/packages/web2/app/components/profile/currentScan.tsx b/packages/web2/app/components/profile/currentScan.tsx index bc28fcf..98d6943 100644 --- a/packages/web2/app/components/profile/currentScan.tsx +++ b/packages/web2/app/components/profile/currentScan.tsx @@ -1,11 +1,11 @@ -import { Route } from "../../routes/profiles.$profileId.tsx"; +import { Route } from "~/routes/profiles.$profileId.tsx"; import { Alert, Align, LoadingSpinner, Text, } from "@mittwald/flow-remote-react-components"; -import { isPending, isRunning } from "./helpers.ts"; +import { isPending, isRunning } from "./helpers"; const RunningScan = () => { return ( @@ -32,7 +32,7 @@ const PendingScan = () => { export const CurrentScan = () => { const { profile: { nextScan }, - } = Route.useLoaderData(); + } = Route.useLoaderData()!; if (!nextScan) { return null; diff --git a/packages/web2/app/components/profile/errorScan.tsx b/packages/web2/app/components/profile/errorScan.tsx index cdf9fc0..7dfe2c5 100644 --- a/packages/web2/app/components/profile/errorScan.tsx +++ b/packages/web2/app/components/profile/errorScan.tsx @@ -1,8 +1,8 @@ import { FC, ReactNode } from "react"; -import { Scan, ScanProfile } from "../../api/types.js"; +import { Scan, ScanProfile } from "~/api/types.js"; import { DefaultErrorView } from "./errorScans/defaultErrorView.js"; import { ErrorViewWithEditDomain } from "./errorScans/ErrorViewWithEditDomain.js"; -import { ErrorViewWithoutEditDomain } from "./errorScans/ErrorViewWithoutEditDomain.js"; +import { ErrorViewWithoutEditDomain } from "./errorScans/ErrorViewWithoutEditDomain"; interface ErrorTexts { headline: string; diff --git a/packages/web2/app/components/profile/errorScans/ErrorViewWithEditDomain.tsx b/packages/web2/app/components/profile/errorScans/ErrorViewWithEditDomain.tsx index efde1e5..03b0190 100644 --- a/packages/web2/app/components/profile/errorScans/ErrorViewWithEditDomain.tsx +++ b/packages/web2/app/components/profile/errorScans/ErrorViewWithEditDomain.tsx @@ -11,7 +11,7 @@ import { useOverlayController, } from "@mittwald/flow-remote-react-components"; import { RestartScanButton } from "./restartScanButton.js"; -import { ChangeDomainModal } from "../modals/changeDomainModal.js"; +import { ChangeDomainModal } from "~/components/profile/modals/changeDomainModal.js"; export const ErrorViewWithEditDomain: FC = ({ profile, diff --git a/packages/web2/app/components/profile/errorScans/restartScanButton.tsx b/packages/web2/app/components/profile/errorScans/restartScanButton.tsx index 38dbf7e..fccff71 100644 --- a/packages/web2/app/components/profile/errorScans/restartScanButton.tsx +++ b/packages/web2/app/components/profile/errorScans/restartScanButton.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { BaseProps } from "./types.js"; import { useRouter } from "@tanstack/react-router"; import { Action, Button } from "@mittwald/flow-remote-react-components"; -import { startScan } from "../../../actions/scan.js"; +import { startScan } from "~/actions/scan"; export const RestartScanButton: FC = ({ profile, scanId }) => { const router = useRouter(); diff --git a/packages/web2/app/components/profile/errorScans/types.ts b/packages/web2/app/components/profile/errorScans/types.ts index b153484..930a10e 100644 --- a/packages/web2/app/components/profile/errorScans/types.ts +++ b/packages/web2/app/components/profile/errorScans/types.ts @@ -1,5 +1,5 @@ import { ReactNode } from "react"; -import { ScanProfile } from "../../../api/types.js"; +import { ScanProfile } from "~/api/types.js"; export interface BaseProps { profile: ScanProfile; diff --git a/packages/web2/app/components/profile/helpers.ts b/packages/web2/app/components/profile/helpers.ts index 27b7128..e9dff4b 100644 --- a/packages/web2/app/components/profile/helpers.ts +++ b/packages/web2/app/components/profile/helpers.ts @@ -1,4 +1,4 @@ -import { Scan } from "../../api/types.ts"; +import { Scan } from "~/api/types"; export const isRunning = (scan: Scan) => { return scan.status === "running"; diff --git a/packages/web2/app/components/profile/modals/EditIntervalModal.tsx b/packages/web2/app/components/profile/modals/EditIntervalModal.tsx index 608054d..8a23ae6 100644 --- a/packages/web2/app/components/profile/modals/EditIntervalModal.tsx +++ b/packages/web2/app/components/profile/modals/EditIntervalModal.tsx @@ -16,10 +16,10 @@ import { Time } from "@internationalized/date"; import { CronInterval, getIntervalValueFromCronSyntax, -} from "../CronFields/lib.js"; -import { ScanProfile } from "../../../api/types.js"; -import { CronFields } from "../CronFields/CronFields.js"; -import { updateProfileCron } from "../../../actions/profile.js"; +} from "~/components/profile/CronFields/lib.js"; +import { ScanProfile } from "~/api/types"; +import { CronFields } from "~/components/profile/CronFields/CronFields.js"; +import { updateProfileCron } from "~/actions/profile"; import { useRouter } from "@tanstack/react-router"; interface Props { diff --git a/packages/web2/app/components/profile/modals/changeDomainModal.tsx b/packages/web2/app/components/profile/modals/changeDomainModal.tsx index 00be052..0eb7bef 100644 --- a/packages/web2/app/components/profile/modals/changeDomainModal.tsx +++ b/packages/web2/app/components/profile/modals/changeDomainModal.tsx @@ -1,4 +1,4 @@ -import { ScanProfile } from "../../../api/types.ts"; +import { ScanProfile } from "~/api/types"; import { useForm } from "react-hook-form"; import { Action, @@ -11,10 +11,10 @@ import { Section, } from "@mittwald/flow-remote-react-components"; import { Form } from "@mittwald/flow-remote-react-components/react-hook-form"; -import { updateProfileDomain } from "../../../actions/profile.ts"; +import { updateProfileDomain } from "~/actions/profile"; import { useRouter } from "@tanstack/react-router"; -import { startScan } from "../../../actions/scan.js"; -import { Domain } from "../../create/components/domain.js"; +import { startScan } from "~/actions/scan"; +import { Domain } from "~/components/create/components/domain.js"; interface FormValues { domain: string; diff --git a/packages/web2/app/components/profile/modals/deleteConfirmation.tsx b/packages/web2/app/components/profile/modals/deleteConfirmation.tsx index 57b1601..f21083c 100644 --- a/packages/web2/app/components/profile/modals/deleteConfirmation.tsx +++ b/packages/web2/app/components/profile/modals/deleteConfirmation.tsx @@ -9,8 +9,8 @@ import { Section, Text, } from "@mittwald/flow-remote-react-components"; -import { ScanProfile } from "../../../api/types.ts"; -import { deleteProfile } from "../../../actions/profile.ts"; +import { ScanProfile } from "~/api/types"; +import { deleteProfile } from "~/actions/profile"; export const DeleteConfirmationModal = ({ profile, diff --git a/packages/web2/app/components/profile/modals/editGenerals.tsx b/packages/web2/app/components/profile/modals/editGenerals.tsx index 39d7035..5fa6637 100644 --- a/packages/web2/app/components/profile/modals/editGenerals.tsx +++ b/packages/web2/app/components/profile/modals/editGenerals.tsx @@ -1,4 +1,4 @@ -import { ScanProfile } from "../../../api/types.ts"; +import { ScanProfile } from "~/api/types"; import { Action, ActionGroup, @@ -21,9 +21,9 @@ import { typedField, } from "@mittwald/flow-remote-react-components/react-hook-form"; import { useRouter } from "@tanstack/react-router"; -import { updateProfileSettings } from "../../../actions/profile.ts"; -import { WcagStandardContextualHelp } from "../wcagStandardContextualHelp.tsx"; -import { CriteriaContextualHelp } from "../criteriaContextualHelp.tsx"; +import { updateProfileSettings } from "~/actions/profile"; +import { WcagStandardContextualHelp } from "~/components/profile/wcagStandardContextualHelp"; +import { CriteriaContextualHelp } from "~/components/profile/criteriaContextualHelp"; interface FormValues { cronExpression?: string; @@ -75,6 +75,7 @@ export const EditGeneralsModal = ({ profile }: { profile: ScanProfile }) => {
+ {/* eslint-disable-next-line react-hooks/static-components -- intended use of Flow Components */} { + {/* eslint-disable-next-line react-hooks/static-components -- intended use of Flow Components */}