diff --git a/.editorconfig b/.editorconfig index 63f92aa4..de2782f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,5 +5,8 @@ indent_style = tab end_of_line = lf insert_final_newline = false +[*.{vue,js}] +indent_style = space + [*.{yml,yaml}] indent_style = space diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..432793cd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (optional):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12fada13..078ae1cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ jobs: with: java-version: 1.11 - uses: actions/checkout@v2 + - if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository ) }} + uses: hallee/eslint-action@1.0.3 + with: + repo-token: "${{secrets.GITHUB_TOKEN}}" + source-root: "client" - run: | git fetch --unshallow - gradle build lint test \ No newline at end of file + gradle build lint test diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..d40b1be6 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,113 @@ +name: Pull Request + +on: + pull_request: + branches: + - dev + - pilot + types: + - opened + - closed + - synchronize + +# Environment variables available to all jobs and steps in this workflow +env: + GKE_PROJECT_ID: ${{ secrets.GKE_PROJECT_ID }} + GKE_EMAIL: ${{ secrets.GKE_EMAIL }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + GITHUB_SHA: ${{ github.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REGISTRY_HOSTNAME: eu.gcr.io + +jobs: + stop-pr: + if: github.event.pull_request.closed == true + name: Stop PR + runs-on: ubuntu-latest + steps: + - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + with: + version: '290.0.1' + service_account_email: ${{ secrets.GKE_EMAIL }} + service_account_key: ${{ secrets.GKE_KEY }} + project_id: ${{ secrets.GKE_PROJECT_ID }} + - name: Stop + run: | + gcloud components install beta + + gcloud run services delete imis-pr-${PR_NUMBER} + + deploy-pr: + if: github.event.pull_request.closed != true + name: Deploy PR + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + - uses: actions/cache@v1 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + # Setup gcloud CLI + - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + with: + version: '290.0.1' + service_account_email: ${{ secrets.GKE_EMAIL }} + service_account_key: ${{ secrets.GKE_KEY }} + project_id: ${{ secrets.GKE_PROJECT_ID }} + - uses: actions/setup-java@v1.3.0 + with: + java-version: 1.11 + # Configure docker to use the gcloud command-line tool as a credential helper + - run: | + # Set up docker to authenticate + # via gcloud command-line tool. + gcloud auth configure-docker + + # Build and push image to Google Container Registry + - name: Build + run: |- + gradle server:test --tests "*GenerateSwagger" -x processResources + gradle client:generateClient + gradle jib --image "$REGISTRY_HOSTNAME/$GKE_PROJECT_ID/imis:$GITHUB_SHA" + + - name: Deploy + run: | + export IMAGE="$REGISTRY_HOSTNAME/$GKE_PROJECT_ID/imis:$GITHUB_SHA" + + gcloud components install beta + + gcloud beta run deploy imis-pr-${PR_NUMBER} --image "$IMAGE" \ + --platform managed \ + --allow-unauthenticated \ + --max-instances=3 \ + --memory=512Mi --cpu=1000m \ + --concurrency=80 \ + --set-env-vars="SPRING_PROFILES_ACTIVE=production,SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD},CLOUD_SQL_INSTANCE=challenge-11" \ + --set-cloudsql-instances=onyx-yeti-271818:europe-west3:challenge-11 \ + --service-account=cloudsql-instance-service-acco@onyx-yeti-271818.iam.gserviceaccount.com \ + --region=europe-west1 + + PR_URL="$(gcloud beta run services describe imis-pr-417 --platform=managed --region=europe-west1 --format=yaml | grep -o 'url:.*' | uniq)" + echo "::set-env name=PR_COMMENT_MESSAGE::${PR_URL}" + + - name: comment PR + uses: unsplash/comment-on-pr@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + msg: "Live preview at ${{ env.PR_COMMENT_MESSAGE }}" + check_for_duplicate_msg: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 78816102..941d8007 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .idea .gradle .DS_Store +client/.vscode .vscode **/.project **/.settings diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 883ea1d0..15df5c9a 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: {{- include "imis.labels" . | nindent 4 }} spec: replicas: 1 + revisionHistoryLimit: 5 selector: matchLabels: {{- include "imis.selectorLabels" . | nindent 6 }} @@ -32,6 +33,10 @@ spec: value: jdbc:postgresql://localhost:5432/{{- .Values.database }} - name: SPRING_DATASOURCE_USERNAME value: postgres + - name: SPRING_CLOUD_GCP_SQL_ENABLED + value: "false" + - name: MANAGEMENT_SERVER_PORT + value: '8081' - name: SPRING_DATASOURCE_PASSWORD valueFrom: secretKeyRef: @@ -41,6 +46,9 @@ spec: - name: cloudstore-service-account mountPath: /usr/local/cloud-store readOnly: true + - name: lucene-index-volume + mountPath: /data + readOnly: false readinessProbe: httpGet: path: /actuator/health @@ -83,3 +91,6 @@ spec: - name: cloudstore-service-account secret: secretName: "{{ include "imis.fullname" . -}}-cloudstore" + - name: lucene-index-volume + emptyDir: {} + diff --git a/client/.eslintrc.js b/client/.eslintrc.js index 74ab81b6..51cb91a3 100644 --- a/client/.eslintrc.js +++ b/client/.eslintrc.js @@ -9,8 +9,8 @@ module.exports = { 'plugin:vue/essential', 'eslint:recommended', '@vue/typescript/recommended', - //'@vue/prettier', - //'@vue/prettier/@typescript-eslint', + '@vue/prettier', + '@vue/prettier/@typescript-eslint', ], parserOptions: { parser: '@typescript-eslint/parser', @@ -19,6 +19,8 @@ module.exports = { rules: { '@typescript-eslint/no-extra-semi': 'error', '@typescript-eslint/no-explicit-any': 'off', + // Does not work with delimiter "none" even though documentation says otherwise: + '@typescript-eslint/member-delimiter-style': 'off', }, overrides: [ { diff --git a/client/src/Root.vue b/client/src/Root.vue index defba808..bd2f642d 100644 --- a/client/src/Root.vue +++ b/client/src/Root.vue @@ -5,7 +5,5 @@ diff --git a/client/src/api/SwaggerApi.ts b/client/src/api/SwaggerApi.ts index 8467af4c..19034b26 100644 --- a/client/src/api/SwaggerApi.ts +++ b/client/src/api/SwaggerApi.ts @@ -94,7 +94,8 @@ export interface CreatePatientDTO { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; phoneNumber?: string; preIllnesses?: string[]; quarantineUntil?: string; @@ -183,7 +184,8 @@ export interface Incident { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; id?: string; patient?: Patient; versionTimestamp?: string; @@ -387,7 +389,8 @@ export interface Patient { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; phoneNumber?: string; preIllnesses?: string[]; quarantineUntil?: string; @@ -433,7 +436,8 @@ export interface PatientEvent { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; id?: string; illness?: "CORONA"; labTest?: LabTest; @@ -474,7 +478,8 @@ export interface PatientSearchParamsDTO { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; phoneNumber?: string; quarantineStatus?: Array< | "REGISTERED" @@ -493,6 +498,7 @@ export interface PatientSearchParamsDTO { | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED" >; street?: string; zip?: string; @@ -526,7 +532,8 @@ export interface QuarantineIncident { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; id?: string; patient?: Patient; until?: string; @@ -568,7 +575,8 @@ export interface RequestQuarantineDTO { | "QUARANTINE_SELECTED" | "QUARANTINE_MANDATED" | "QUARANTINE_RELEASED" - | "QUARANTINE_PROFESSIONBAN_RELEASED"; + | "QUARANTINE_PROFESSIONBAN_RELEASED" + | "HOSPITALIZATION_MANDATED"; } export interface SendToQuarantineDTO { @@ -865,6 +873,22 @@ export class Api extends HttpClient { updateExposureContactUsingPut: (contact: ExposureContactToServer, params?: RequestParams) => this.request(`/api/exposure-contacts`, "PUT", params, contact, true), + /** + * @tags exposure-contact-controller + * @name getExposureSourceContactsForPatientsUsingPOST + * @summary getExposureSourceContactsForPatients + * @request POST:/api/exposure-contacts/by-contact/ + * @secure + */ + getExposureSourceContactsForPatientsUsingPost: (patientIds: string[], params?: RequestParams) => + this.request, any>( + `/api/exposure-contacts/by-contact/`, + "POST", + params, + patientIds, + true, + ), + /** * @tags exposure-contact-controller * @name getExposureSourceContactsForPatientUsingGET @@ -967,6 +991,20 @@ export class Api extends HttpClient { getLogUsingGet: (id: string, params?: RequestParams) => this.request(`/api/incidents/${id}/log`, "GET", params, null, true), + /** + * @tags incident-controller + * @name getPatientsCurrentByTypeUsingPOST + * @summary getPatientsCurrentByType + * @request POST:/api/incidents/{type}/patient + * @secure + */ + getPatientsCurrentByTypeUsingPost: ( + type: "test" | "quarantine" | "administrative", + patientIds: string[], + params?: RequestParams, + ) => + this.request, any>(`/api/incidents/${type}/patient`, "POST", params, patientIds, true), + /** * @tags incident-controller * @name getPatientCurrentByTypeUsingGET @@ -976,7 +1014,7 @@ export class Api extends HttpClient { */ getPatientCurrentByTypeUsingGet: ( id: string, - type: "test" | "quarantine" | "administrative", + type: "test" | "quarantine" | "administrative" | "hospitalization", params?: RequestParams, ) => this.request(`/api/incidents/${type}/patient/${id}`, "GET", params, null, true), @@ -987,8 +1025,11 @@ export class Api extends HttpClient { * @request GET:/api/incidents/{type}/patient/{id}/log * @secure */ - getPatientLogByTypeUsingGet: (id: string, type: "test" | "quarantine" | "administrative", params?: RequestParams) => - this.request(`/api/incidents/${type}/patient/${id}/log`, "GET", params, null, true), + getPatientLogByTypeUsingGet: ( + id: string, + type: "test" | "quarantine" | "administrative" | "hospitalization", + params?: RequestParams, + ) => this.request(`/api/incidents/${type}/patient/${id}/log`, "GET", params, null, true), /** * @tags institution-controller @@ -1270,70 +1311,72 @@ export class Api extends HttpClient { error = { /** * @tags basic-error-controller - * @name errorHtmlUsingGET - * @summary errorHtml + * @name errorUsingGET + * @summary error * @request GET:/error * @secure */ - errorHtmlUsingGet: (params?: RequestParams) => this.request(`/error`, "GET", params, null, true), + errorUsingGet: (params?: RequestParams) => + this.request, any>(`/error`, "GET", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingHEAD - * @summary errorHtml + * @name errorUsingHEAD + * @summary error * @request HEAD:/error * @secure */ - errorHtmlUsingHead: (params?: RequestParams) => - this.request(`/error`, "HEAD", params, null, true), + errorUsingHead: (params?: RequestParams) => + this.request, any>(`/error`, "HEAD", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingPOST - * @summary errorHtml + * @name errorUsingPOST + * @summary error * @request POST:/error * @secure */ - errorHtmlUsingPost: (params?: RequestParams) => - this.request(`/error`, "POST", params, null, true), + errorUsingPost: (params?: RequestParams) => + this.request, any>(`/error`, "POST", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingPUT - * @summary errorHtml + * @name errorUsingPUT + * @summary error * @request PUT:/error * @secure */ - errorHtmlUsingPut: (params?: RequestParams) => this.request(`/error`, "PUT", params, null, true), + errorUsingPut: (params?: RequestParams) => + this.request, any>(`/error`, "PUT", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingDELETE - * @summary errorHtml + * @name errorUsingDELETE + * @summary error * @request DELETE:/error * @secure */ - errorHtmlUsingDelete: (params?: RequestParams) => - this.request(`/error`, "DELETE", params, null, true), + errorUsingDelete: (params?: RequestParams) => + this.request, any>(`/error`, "DELETE", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingOPTIONS - * @summary errorHtml + * @name errorUsingOPTIONS + * @summary error * @request OPTIONS:/error * @secure */ - errorHtmlUsingOptions: (params?: RequestParams) => - this.request(`/error`, "OPTIONS", params, null, true), + errorUsingOptions: (params?: RequestParams) => + this.request, any>(`/error`, "OPTIONS", params, null, true), /** * @tags basic-error-controller - * @name errorHtmlUsingPATCH - * @summary errorHtml + * @name errorUsingPATCH + * @summary error * @request PATCH:/error * @secure */ - errorHtmlUsingPatch: (params?: RequestParams) => - this.request(`/error`, "PATCH", params, null, true), + errorUsingPatch: (params?: RequestParams) => + this.request, any>(`/error`, "PATCH", params, null, true), }; } diff --git a/client/src/api/index.ts b/client/src/api/index.ts index d8657864..4735d0a1 100644 --- a/client/src/api/index.ts +++ b/client/src/api/index.ts @@ -36,7 +36,8 @@ const apiWrapper = { }), } -function createApiProxy(foo: Api['api']): Api['api'] { // Proxy is compatible with Foo +function createApiProxy(foo: Api['api']): Api['api'] { + // Proxy is compatible with Foo const handler = { get: (target: Api['api'], prop: keyof Api['api'], receiver: any) => { if (apiWrapper.apiInstance.api[prop] !== null) { diff --git a/client/src/assets/global.scss b/client/src/assets/global.scss index 7216aa39..54b6f368 100644 --- a/client/src/assets/global.scss +++ b/client/src/assets/global.scss @@ -60,7 +60,8 @@ body { } h4 { - font-weight: bold, + font-weight: bold; + margin-bottom: 1em; } .fading-enter-active, diff --git a/client/src/components/AddOrChangeUserForm.vue b/client/src/components/AddOrChangeUserForm.vue index fd7a70fe..03dbc94c 100644 --- a/client/src/components/AddOrChangeUserForm.vue +++ b/client/src/components/AddOrChangeUserForm.vue @@ -2,55 +2,95 @@ - - + Admin Regular @@ -93,36 +133,40 @@ export default Vue.extend({ if (this.isNewUser) { this.registerUserForInstitution({ ...values, - }).then(() => { - this.$notification.success({ - message: 'Benutzer erfolgreich angelegt', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Benutzer erfolgreich angelegt', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Benutzer konnte nicht angelegt werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Benutzer konnte nicht angelegt werden.', + description: error.message, + }) }) - }) } else { this.updateUserForInstitution({ id: this.user.id, ...values, - }).then(() => { - this.$notification.success({ - message: 'Benutzer erfolgreich aktualisiert', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Benutzer erfolgreich aktualisiert', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Benutzer konnte nicht aktualisiert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Benutzer konnte nicht aktualisiert werden.', + description: error.message, + }) }) - }) } }) }, @@ -130,5 +174,4 @@ export default Vue.extend({ }) - + diff --git a/client/src/components/BarcodeScanner.vue b/client/src/components/BarcodeScanner.vue index 3dccc220..afd4b6f1 100644 --- a/client/src/components/BarcodeScanner.vue +++ b/client/src/components/BarcodeScanner.vue @@ -3,25 +3,33 @@
-

{{result}}

+

{{ result }}

- Scan new result + Scan new result Use
- - {{d.label}} + + {{ + d.label + }}
Cancel
- diff --git a/client/src/components/ChangeInstitutionForm.vue b/client/src/components/ChangeInstitutionForm.vue index ce985be2..3a94c56a 100644 --- a/client/src/components/ChangeInstitutionForm.vue +++ b/client/src/components/ChangeInstitutionForm.vue @@ -4,60 +4,109 @@ title="Institution ändern" okText="Speichern" cancelText="Abbrechen" - @cancel="() => { $emit('cancel') }" + @cancel=" + () => { + $emit('cancel') + } + " @ok="save" > - + - +
- +
- +
@@ -107,24 +156,25 @@ export default Vue.extend({ ...values, institutionType: this.institution.institutionType, id: this.institution.id, - }).then(() => { - this.$notification.success({ - message: 'Institution erfolgreich geändert', - description: '', + }) + .then(() => { + this.$notification.success({ + message: 'Institution erfolgreich geändert', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Institution konnte nicht geändert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Institution konnte nicht geändert werden.', + description: error.message, + }) }) - }) }) }, }, }) - + diff --git a/client/src/components/ChangePasswordForm.vue b/client/src/components/ChangePasswordForm.vue index 12f091e7..be841480 100644 --- a/client/src/components/ChangePasswordForm.vue +++ b/client/src/components/ChangePasswordForm.vue @@ -1,15 +1,16 @@ @@ -50,25 +55,26 @@ export default Vue.extend({ values.dateOfDeath = values.dateOfDeath.format('YYYY-MM-DD') } values = { ...this.patient, ...values } - Api.updatePatientUsingPut(values).then((updatedPatient) => { - this.setPatient(updatedPatient) - this.$notification.success({ - message: 'Patient erfolgreich aktualisiert', - description: '', + Api.updatePatientUsingPut(values) + .then((updatedPatient) => { + this.setPatient(updatedPatient) + this.$notification.success({ + message: 'Patient erfolgreich aktualisiert', + description: '', + }) + this.form.resetFields() + this.$emit('create') }) - this.form.resetFields() - this.$emit('create') - }).catch((error: Error) => { - this.$notification.error({ - message: 'Patient konnte nicht aktualisiert werden.', - description: error.message, + .catch((error: Error) => { + this.$notification.error({ + message: 'Patient konnte nicht aktualisiert werden.', + description: error.message, + }) }) - }) }) }, }, }) - + diff --git a/client/src/components/DateInput.vue b/client/src/components/DateInput.vue index 59adf393..1afd743d 100644 --- a/client/src/components/DateInput.vue +++ b/client/src/components/DateInput.vue @@ -13,10 +13,8 @@ - + diff --git a/client/src/components/EditExposureContact.vue b/client/src/components/EditExposureContact.vue index 5e3ad119..8e62e64f 100644 --- a/client/src/components/EditExposureContact.vue +++ b/client/src/components/EditExposureContact.vue @@ -1,75 +1,86 @@