diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d3dbac34 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.github +.husky +cypress +node_modules diff --git a/Dockerfile.app b/Dockerfile.app index 4799a914..a5a8cfd6 100644 --- a/Dockerfile.app +++ b/Dockerfile.app @@ -7,9 +7,16 @@ ARG VITE_SENTRY_ENVIRONMENT ARG VITE_SENTRY_RELEASE WORKDIR /app RUN mkdir -p confiture-web-app/src/assets +RUN mkdir -p confiture-web-app/src/types COPY package.json yarn.lock CHANGELOG.md ROADMAP.md . COPY confiture-web-app/package.json confiture-web-app/ +COPY confiture-rest-api/ confiture-rest-api/ +RUN pwd +RUN ls RUN yarn install --frozen-lockfile --non-interactive --production=false +RUN ls confiture-web-app/src/types + + WORKDIR /app/confiture-web-app COPY confiture-web-app/ . RUN VITE_MATOMO_ENABLE=1 SENTRY_ORG=${SENTRY_ORG} SENTRY_PROJECT=${SENTRY_PROJECT} SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} VITE_SENTRY_DSN=${VITE_SENTRY_DSN} VITE_SENTRY_ENVIRONMENT=${VITE_SENTRY_ENVIRONMENT} VITE_SENTRY_RELEASE=${VITE_SENTRY_RELEASE} yarn build diff --git a/confiture-rest-api/.gitignore b/confiture-rest-api/.gitignore index b6bb8bc1..47c00b7a 100644 --- a/confiture-rest-api/.gitignore +++ b/confiture-rest-api/.gitignore @@ -36,3 +36,6 @@ lerna-debug.log* .env src/generated + +# Generated API typings +confiture-api.ts diff --git a/confiture-rest-api/package.json b/confiture-rest-api/package.json index 0e958359..4607bec2 100644 --- a/confiture-rest-api/package.json +++ b/confiture-rest-api/package.json @@ -12,7 +12,8 @@ "lint": "eslint \"src/**/*.ts\" --fix", "migrate:dev": "prisma migrate dev", "migrate:prod": "prisma migrate deploy", - "postinstall": "prisma generate" + "postinstall": "yarn generate-api-types", + "generate-api-types": "prisma generate && rimraf dist && GENERATE_TYPES=1 nest start --entryFile generate-api-typings.js" }, "dependencies": { "@aws-sdk/client-s3": "^3.218.0", @@ -62,6 +63,7 @@ "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-simple-import-sort": "^12.1.0", "eslint-plugin-unused-imports": "^3.1.0", + "openapi-typescript": "^6.7.6", "prettier": "^3.1.1", "source-map-support": "^0.5.20", "ts-loader": "^9.2.3", diff --git a/confiture-rest-api/prisma/schema.prisma b/confiture-rest-api/prisma/schema.prisma index 3c1a51b2..c21ee1b5 100644 --- a/confiture-rest-api/prisma/schema.prisma +++ b/confiture-rest-api/prisma/schema.prisma @@ -102,7 +102,9 @@ model TestEnvironment { assistiveTechnology String browser String + /// @DtoEntityHidden audit Audit? @relation(fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade) + /// @DtoEntityHidden auditUniqueId String? @@unique([platform, operatingSystem, assistiveTechnology, browser, auditUniqueId]) @@ -115,12 +117,15 @@ model AuditedPage { url String // parent audit when the page is a user made page + /// @DtoEntityHidden audit Audit? @relation(name: "UserPages", fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade) auditUniqueId String? // parent audit when the page is a transverse page + /// @DtoEntityHidden auditTransverse Audit? @relation(name: "TransversePage") + /// @DtoEntityHidden results CriterionResult[] } @@ -184,7 +189,9 @@ model StoredFile { key String thumbnailKey String + /// @DtoEntityHidden criterionResult CriterionResult? @relation(fields: [criterionResultId], references: [id], onDelete: Cascade, onUpdate: Cascade) + /// @DtoEntityHidden criterionResultId Int? } @@ -203,7 +210,9 @@ model AuditFile { key String thumbnailKey String? + /// @DtoEntityHidden audit Audit? @relation(fields: [auditUniqueId], references: [editUniqueId], onDelete: Cascade) + /// @DtoEntityHidden auditUniqueId String? } diff --git a/confiture-rest-api/src/app.module.ts b/confiture-rest-api/src/app.module.ts index dd870543..2d48b1c7 100644 --- a/confiture-rest-api/src/app.module.ts +++ b/confiture-rest-api/src/app.module.ts @@ -15,7 +15,9 @@ import { PrismaService } from "./prisma.service"; imports: [ ConfigModule.forRoot({ isGlobal: true, - validationSchema: configValidationSchema + validationSchema: !process.env.GENERATE_TYPES + ? configValidationSchema + : undefined }), FeedbackModule, AuditsModule, diff --git a/confiture-rest-api/src/audits/audit.service.ts b/confiture-rest-api/src/audits/audit.service.ts index 5088ea34..3d0c51f2 100644 --- a/confiture-rest-api/src/audits/audit.service.ts +++ b/confiture-rest-api/src/audits/audit.service.ts @@ -20,6 +20,7 @@ import { FileStorageService } from "./file-storage.service"; import { UpdateAuditDto } from "./dto/update-audit.dto"; import { UpdateResultsDto } from "./dto/update-results.dto"; import { PatchAuditDto } from "./dto/patch-audit.dto"; +import { AuditListingItemDto } from "./dto/audit-listing-item.dto"; const AUDIT_EDIT_INCLUDE = { recipients: true, @@ -1287,7 +1288,7 @@ export class AuditService { }); } - async getAuditsByAuditorEmail(email: string) { + async getAuditsByAuditorEmail(email: string): Promise { const audits = await this.prisma.audit.findMany({ where: { auditorEmail: email, @@ -1314,7 +1315,7 @@ export class AuditService { } }); - const unorderedAudits = audits.map((a) => { + const unorderedAudits: AuditListingItemDto[] = audits.map((a) => { const allResults = [ ...a.transverseElementsPage.results, ...a.pages.flatMap((p) => p.results) diff --git a/confiture-rest-api/src/audits/audits.controller.ts b/confiture-rest-api/src/audits/audits.controller.ts index e9bcfad3..38b7de12 100644 --- a/confiture-rest-api/src/audits/audits.controller.ts +++ b/confiture-rest-api/src/audits/audits.controller.ts @@ -39,6 +39,8 @@ import { UploadImageDto } from "./dto/upload-image.dto"; import { AuthRequired } from "src/auth/auth-required.decorator"; import { User } from "src/auth/user.decorator"; import { AuthenticationJwtPayload } from "src/auth/jwt-payloads"; +import { AuditListingItemDto } from "./dto/audit-listing-item.dto"; +import { StoredFile } from "src/generated/nestjs-dto/storedFile.entity"; @Controller("audits") @ApiTags("Audits") @@ -76,6 +78,7 @@ export class AuditsController { */ @Get() @AuthRequired() + @ApiOkResponse({ type: AuditListingItemDto, isArray: true }) async getAuditList(@User() user: AuthenticationJwtPayload) { return this.auditService.getAuditsByAuditorEmail(user.email); } @@ -146,6 +149,7 @@ export class AuditsController { @Post("/:uniqueId/results/examples") @UseInterceptors(FileInterceptor("image")) + @ApiCreatedResponse({ type: StoredFile }) async uploadExampleImage( @Param("uniqueId") uniqueId: string, @UploadedFile( diff --git a/confiture-rest-api/src/audits/dto/audit-listing-item.dto.ts b/confiture-rest-api/src/audits/dto/audit-listing-item.dto.ts new file mode 100644 index 00000000..159bd815 --- /dev/null +++ b/confiture-rest-api/src/audits/dto/audit-listing-item.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { AuditType } from "@prisma/client"; + +export class AuditListingItemDto { + procedureName: string; + editUniqueId: string; + consultUniqueId: string; + creationDate: Date; + @ApiProperty({ enum: AuditType }) + auditType: AuditType; + complianceLevel: number; + @ApiProperty({ enum: ["NOT_STARTED", "COMPLETED", "IN_PROGRESS"] }) + status: "NOT_STARTED" | "COMPLETED" | "IN_PROGRESS"; + estimatedCsvSize: number; + statementIsPublished: boolean; +} diff --git a/confiture-rest-api/src/audits/dto/audit-report.dto.ts b/confiture-rest-api/src/audits/dto/audit-report.dto.ts index 0ac3e5be..ec457a70 100644 --- a/confiture-rest-api/src/audits/dto/audit-report.dto.ts +++ b/confiture-rest-api/src/audits/dto/audit-report.dto.ts @@ -37,24 +37,7 @@ export class AuditReportDto { */ accessibilityRate: number; - /** - * @example { - * total: 106; - * compliant: 30; - * notCompliant: 46; - * blocking: 12; - * applicable: 76; - * notApplicable: 30; - * } - */ - criteriaCount: { - total: number; - compliant: number; - notCompliant: number; - blocking: number; - applicable: number; - notApplicable: number; - }; + criteriaCount: CriteriaCount; /** Global distribution of criteria by result */ resultDistribution: ResultDistribution; @@ -68,6 +51,21 @@ export class AuditReportDto { results: ReportCriterionResult[]; } +class CriteriaCount { + /** @example 106 */ + total: number; + /** @example 30 */ + compliant: number; + /** @example 46 */ + notCompliant: number; + /** @example 12 */ + blocking: number; + /** @example 76 */ + applicable: number; + /** @example 30 */ + notApplicable: number; +} + class RawAndPercentage { /** * @example 47 @@ -187,6 +185,8 @@ class ReportCriterionResult { userImpact: CriterionResultUserImpact | null; notApplicableComment: string | null; + + quickWin: boolean; } class ExampleImage { diff --git a/confiture-rest-api/src/generate-api-typings.ts b/confiture-rest-api/src/generate-api-typings.ts new file mode 100644 index 00000000..9ef4b2be --- /dev/null +++ b/confiture-rest-api/src/generate-api-typings.ts @@ -0,0 +1,24 @@ +import { NestFactory } from "@nestjs/core"; +import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; +import { writeFile } from "fs/promises"; +import openapiTS, { OpenAPI3 } from "openapi-typescript"; +import { resolve } from "path"; + +import { AppModule } from "./app.module"; + +async function main() { + const app = await NestFactory.create(AppModule); + + const config = new DocumentBuilder().setTitle("Confiture API").build(); + const document = SwaggerModule.createDocument(app, config); + + const ast = await openapiTS(document as OpenAPI3); + const fileContent = "/* eslint-disable */\n" + ast; + const resolvedPath = resolve(__dirname, "../confiture-api.ts"); + await writeFile(resolvedPath, fileContent, { + encoding: "utf-8" + }); + console.log("✅ Typings saved to", resolvedPath); +} + +main(); diff --git a/confiture-rest-api/tsconfig.build.json b/confiture-rest-api/tsconfig.build.json index 052134cd..82089442 100644 --- a/confiture-rest-api/tsconfig.build.json +++ b/confiture-rest-api/tsconfig.build.json @@ -1,4 +1,11 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "scripts"] + "exclude": [ + "node_modules", + "test", + "dist", + "**/*spec.ts", + "scripts", + "confiture-api.ts" + ] } diff --git a/confiture-web-app/.dockerignore b/confiture-web-app/.dockerignore index 06f17afd..325a1db3 100644 --- a/confiture-web-app/.dockerignore +++ b/confiture-web-app/.dockerignore @@ -2,4 +2,5 @@ node_modules accessibilite.numerique.gouv.fr coverage dist -.vscode \ No newline at end of file +.vscode +src/types/confiture-api.ts diff --git a/confiture-web-app/.gitignore b/confiture-web-app/.gitignore index eecbde4e..75d821b8 100644 --- a/confiture-web-app/.gitignore +++ b/confiture-web-app/.gitignore @@ -34,3 +34,4 @@ coverage accessibilite.numerique.gouv.fr src/methodologies.json src/criteres.json +src/types/confiture-api.ts \ No newline at end of file diff --git a/confiture-web-app/src/components/account/settings/Profile.vue b/confiture-web-app/src/components/account/settings/Profile.vue index 1376666c..f2bca03c 100644 --- a/confiture-web-app/src/components/account/settings/Profile.vue +++ b/confiture-web-app/src/components/account/settings/Profile.vue @@ -37,8 +37,8 @@ const showActions = computed(() => { function updateProfile() { accountStore .updateProfile({ - name: name.value || null, - orgName: orgName.value || null + name: name.value, + orgName: orgName.value }) .then(() => { notify("success", undefined, "Profil mis à jour avec succès"); diff --git a/confiture-web-app/src/components/report/ReportErrors.vue b/confiture-web-app/src/components/report/ReportErrors.vue index 937afbb3..708709ca 100644 --- a/confiture-web-app/src/components/report/ReportErrors.vue +++ b/confiture-web-app/src/components/report/ReportErrors.vue @@ -2,7 +2,11 @@ import { computed, ref } from "vue"; import { useReportStore } from "../../store"; -import { CriterionResultUserImpact, CriteriumResultStatus } from "../../types"; +import { + CriterionResultUserImpact, + CriteriumResultStatus, + ReportUserImpact +} from "../../types"; import { getReportErrors } from "./getReportErrors"; import ReportCriteria from "./ReportCriteria.vue"; import ReportErrorCriterium from "./ReportErrorCriterium.vue"; @@ -17,7 +21,7 @@ const defaultUserImpactFillters = [ null ]; -const userImpactFilters = ref>( +const userImpactFilters = ref>( defaultUserImpactFillters ); diff --git a/confiture-web-app/src/components/report/getReportErrors.ts b/confiture-web-app/src/components/report/getReportErrors.ts index 31cc50f7..3fb6188c 100644 --- a/confiture-web-app/src/components/report/getReportErrors.ts +++ b/confiture-web-app/src/components/report/getReportErrors.ts @@ -4,9 +4,9 @@ import rgaa from "../../criteres.json"; import { ReportStoreState } from "../../store"; import { AuditReport, - CriterionResultUserImpact, CriteriumResultStatus, - ReportCriteriumResult + ReportCriteriumResult, + ReportUserImpact } from "../../types"; export type ReportError = { @@ -24,7 +24,7 @@ export type ReportError = { export function getReportErrors( report: ReportStoreState, quickWinFilter: boolean, - userImpactFilters: Array + userImpactFilters: Array ): ReportError[] { const resultsGroupedByPage = { // include pages with no errors diff --git a/confiture-web-app/src/components/report/getReportImprovements.ts b/confiture-web-app/src/components/report/getReportImprovements.ts index a0c5d8d3..bee5ae7d 100644 --- a/confiture-web-app/src/components/report/getReportImprovements.ts +++ b/confiture-web-app/src/components/report/getReportImprovements.ts @@ -63,7 +63,7 @@ export function getReportImprovements( improvements: sortBy( results.filter(hasImprovement).map(getImprovementObject), "criterium" - ) + ) as ReportImprovement["topics"][0]["improvements"] }; }) ), diff --git a/confiture-web-app/src/components/ui/FileUpload.vue b/confiture-web-app/src/components/ui/FileUpload.vue index 86890819..8e3fe690 100644 --- a/confiture-web-app/src/components/ui/FileUpload.vue +++ b/confiture-web-app/src/components/ui/FileUpload.vue @@ -4,12 +4,14 @@ import { computed, Ref, ref } from "vue"; import { useIsOffline } from "../../composables/useIsOffline"; import { useUniqueId } from "../../composables/useUniqueId"; import { FileErrorMessage } from "../../enums"; -import { AuditFile } from "../../types"; +import { AuditFile, NotesFile } from "../../types"; import { formatBytes, getUploadUrl } from "../../utils"; +type ComponentFile = NotesFile | AuditFile; + export interface Props { acceptedFormats?: Array; - auditFiles: AuditFile[]; + auditFiles: ComponentFile[]; errorMessage?: FileErrorMessage | null; maxFileSize?: string; multiple?: boolean; @@ -84,21 +86,21 @@ function deleteFile(file: AuditFile) { emit("delete-file", file); } -function getFileName(auditFile: AuditFile) { +function getFileName(auditFile: ComponentFile) { return auditFile.originalFilename; } -function getFullFileName(auditFile: AuditFile) { +function getFullFileName(auditFile: ComponentFile) { return getFileName(auditFile) + " (" + getFileDetails(auditFile) + ")"; } -function getFileDetails(auditFile: AuditFile) { +function getFileDetails(auditFile: ComponentFile) { const name = auditFile.originalFilename; const extension = name.substring(name.lastIndexOf(".") + 1).toUpperCase(); return extension + " – " + formatBytes(auditFile.size); } -function isViewable(auditFile: AuditFile) { +function isViewable(auditFile: ComponentFile) { return ( auditFile.mimetype.startsWith("image") || auditFile.mimetype.includes("pdf") ); @@ -158,7 +160,7 @@ function onFileRequestFinished() {
    -
  • +
  • Supprimer {{ getFullFileName(auditFile) }} diff --git a/confiture-web-app/src/pages/FeedbackPage.vue b/confiture-web-app/src/pages/FeedbackPage.vue index 81c17864..35ec73ca 100644 --- a/confiture-web-app/src/pages/FeedbackPage.vue +++ b/confiture-web-app/src/pages/FeedbackPage.vue @@ -11,8 +11,12 @@ import PageMeta from "../components/PageMeta"; import DsfrField from "../components/ui/DsfrField.vue"; import { useNotifications } from "../composables/useNotifications"; import { usePreviousRoute } from "../composables/usePreviousRoute"; +import { paths } from "../types/confiture-api"; import { captureWithPayloads } from "../utils"; +export type CreateFeedbackRequestData = + paths["/feedback"]["post"]["requestBody"]["content"]["application/json"]; + const availableRadioAnswers = [ { label: "Oui", slug: "yes", emoji: emojiYes }, { label: "Moyen", slug: "medium", emoji: emojiMedium }, @@ -29,14 +33,14 @@ const availableJobs = [ "Autre" ]; -const easyToUse = ref(""); -const easyToUnderstand = ref(""); +const easyToUse = ref(); +const easyToUnderstand = ref(); const feedback = ref(""); const suggestions = ref(""); const contact = ref(); const name = ref(""); const email = ref(""); -const occupations = ref([]); +const occupations = ref([]); const showSuccess = ref(false); @@ -46,18 +50,23 @@ const notify = useNotifications(); * Submit form and display success notice */ function submitFeedback() { + const body: CreateFeedbackRequestData = { + easyToUse: easyToUse.value!, + easyToUnderstand: easyToUnderstand.value!, + feedback: feedback.value, + suggestions: suggestions.value, + ...(contact.value === "yes" && { + email: email.value, + name: name.value, + occupations: + // FIXME: the @nestjs/swagger CLI plugin generating the API types doesnt seem to pick up on the each option + // see: https://github.com/nestjs/swagger/issues/2027 + occupations.value as unknown as CreateFeedbackRequestData["occupations"] + }) + }; + ky.post("/api/feedback", { - json: { - easyToUse: easyToUse.value, - easyToUnderstand: easyToUnderstand.value, - feedback: feedback.value, - suggestions: suggestions.value, - ...(contact.value === "yes" && { - email: email.value, - name: name.value, - occupations: occupations.value - }) - } + json: body }) .then(() => { showSuccess.value = true; @@ -130,6 +139,7 @@ const previousPageName = type="radio" name="easyToUse" :value="answer.label" + required />