3Squared / ForgeUI

ForgeUI
https://3squared.github.io/ForgeUI/#
7 stars 1 forks source link

Forge file editible file validation issue #124

Open izzymallon opened 1 year ago

izzymallon commented 1 year ago

We potentially need some validation around the inline editor within the Forge file uploader. When edit the file name delete the string it will go into error state, but this isn't currently being surfaced and picked up by the ValidationObserver wrapper within EDS. This means you can press save and the when the file uploader is in an invalid state. We need to pass the invalid state from the inline editor up to the parent component.

image

TomSmith27 commented 1 year ago

@izzymallon can you add the code to from EDS to reproduce this?

izzymallon commented 1 year ago

I have added the whole modal component in which maybe isn't helpful. But the problem is in trying to prevent the confirm method, when that first Evidence 'b-tab' is invalid because the inline editor is still open but in an invalid state. Current is just saves and takes the last valid name.

<template>
  <forge-modal
      v-bind="$attrs"
      :static="true"
      :onConfirm="confirm"
      ok-title="Save"
      @shown="shown"
      @hidden="loading = true"
      title="Upload Evidence"
      scrollable
      size="xl"
      legacy
  >
    <template #modal-header>
      <custom-modal-header :title="isEdit ? 'Edit Evidence' : 'Upload Evidence'">
        <small v-if="!loading"
        >{{ selectedUnits }} Units, {{ selectedElements }} Elements, {{ selectedCriteria }} Criteria selected</small
        >
      </custom-modal-header>
    </template>
    <Loader v-if="loading"></Loader>
    <div v-else>
      <ValidationObserver ref="observer" v-slot="{invalid, validated }">
        <b-tabs content-class="mt-4" v-model="activeTab">
          <b-tab
              title="Evidence"
              active
              :title-link-class="
               (noFilesSelected && validated && !isEdit) ? 'text-danger border-danger' : ''
            "
          >

            <forge-form-field v-if="isEdit" label="Filename" class="required" rules="required|max:500">
              <template #default="{ state }">
                <b-form-input v-model="assessmentFile.originalFileName" :state="state" id="filename" />
              </template>
            </forge-form-field>

            <forge-form-field label="" rules="required" v-else>
              <template #default="{ state }">
                <forge-file-upload
                    :class="noFilesSelected && validated ? 'invalid-files' : ''"
                    :multiple="false"
                    :accepted-file-types="acceptedFileTypes"
                    :get-file-url-action="getNewFileUrl"
                    :max-file-size="maxFileSize"
                    editable-file-name
                    :validate-file-name="validateFileName"
                    v-model="filesSelected"
                    :state="state"
                    ref="fileUploader"
                >
                  <template #message>
                    <div class="message-text">
                      Select File to add to the Evidence Library. You may only add one at a time.
                      <accepted-file-types-tooltip />
                    </div>
                  </template>
                </forge-file-upload>
              </template>
            </forge-form-field>
          </b-tab>
          <b-tab
              title="Criteria"
              :title-link-class="noCriteriaSelected && validated ? 'text-danger border-danger' : ''"
          >
            <div class="p-2 pb-4">
              Select the Suggested Criteria for this piece of evidence. You must select at least one unit, element or
              criteria.
            </div>
            <b-card>
              <basic-criteria-tree
                  :unitTags="unitTags"
                  :elementTags="elementTags"
                  :criteriaTags="criteriaTags"
                  :units="units"
              />
            </b-card>
          </b-tab>
          <b-tab title="Description"
                 :title-link-class="(invalid || noDescription) && validated ? 'text-danger border-danger' : ''"
          >
            <forge-form-field label="" class="required" rules="required|max:2000" type="textarea">
              <template #default="{ state }">
                <div class="d-flex justify-content-between">
                  <label class="required-label" for="description">Description</label>
                  <forge-remaining-characters
                      :max-count="2000"
                      :current-count="assessmentFile.description ? assessmentFile.description.length : 0"
                  />
                </div>
                <b-form-textarea id="description" v-model="assessmentFile.description" :state="state" rows="4" />
              </template>
            </forge-form-field>
            <forge-form-field label="" rules="max:2000">
              <template #default="{ state }">
                <div class="d-flex justify-content-between">
                  <label for="done-differently">What could you have done differently?</label>
                  <forge-remaining-characters
                      :max-count="2000"
                      :current-count="
                      assessmentFile.whatCouldYouHaveDoneDifferently
                        ? assessmentFile.whatCouldYouHaveDoneDifferently.length
                        : 0
                    "
                  />
                </div>
                <b-form-textarea
                    id="done-differently"
                    v-model="assessmentFile.whatCouldYouHaveDoneDifferently"
                    :state="state"
                    rows="3"
                />
              </template>
            </forge-form-field>
          </b-tab>
        </b-tabs>
      </ValidationObserver>
    </div>
  </forge-modal>
</template>

<script lang="ts">
import Vue, { VueConstructor } from "vue";
import { assessmentService, evidenceLibraryService } from "@/services/services";
import { ForgeFileStatus, ValidationObserver, ValidationResult } from "@3squared/forge-ui";
import {
  AssessmentFileTaggedCriteriaDto,
  AssessmentFileTaggedElementDto,
  AssessmentFileTaggedUnitDto,
  AssessmentFileWithTagsDto,
  CompetencyUnitNotApplicableDto,
  FeatureName,
  FileTransferInformation,
  PostAssessmentFileWithTagsDto,
  PutAssessmentFileWithTagsDto,
} from "@/models/api-dtos";
import { CriteriaNode, ElementNode, FileStatus, UnitNode } from "@/models/custom";
import BasicCriteriaTree from "./components/BasicCriteriaTree.vue";
import { getTreeList } from "@/utilities/uploadEvidenceUtilities";
import { handleError } from "@/utilities/errorUtility";
import { FileType } from "@/constants/FileTypes";
import Loader from "@/components/shared/Loader.vue";
import AcceptedFileTypesTooltip from "@/components/shared/AcceptedFileTypesTooltip.vue";
import { featureEnabled } from "@/utilities/featureUtility";
import CustomModalHeader from "@/components/shared/CustomModalHeader.vue";
import { maskName } from "@/utilities/filenameUtilities";

export default (
    Vue as VueConstructor<
        Vue & {
      $refs: {
        fileUploader: { files: FileStatus[] };
        observer: InstanceType<typeof ValidationObserver>;
      };
    }
    >
).extend({
  name: "UploadEvidenceModal",
  components: {
    CustomModalHeader,
    AcceptedFileTypesTooltip,
    ValidationObserver,
    BasicCriteriaTree,
    Loader,
  },
  props: {
    competencyCycleId: {
      type: String,
      required: false,
    },
    userId: {
      type: String,
      required: true,
    },
    companyRoleId: {
      type: String,
      required: false,
    },
    assessmentFileId: {
      type: String,
      required: false,
      default: null
    }
  },
  data: () => {
    return {
      loading: true as boolean,
      units: [] as UnitNode[],
      unitTags: [] as AssessmentFileTaggedUnitDto[] | null,
      elementTags: [] as AssessmentFileTaggedElementDto[] | null,
      criteriaTags: [] as AssessmentFileTaggedCriteriaDto[] | null,
      filesSelected: [] as ForgeFileStatus[],
      acceptedFileTypes: Object.values(FileType).toString(),
      maxFileSize: 50000000,
      activeTab: 0,
      competencyUnitsNotApplicable: [] as CompetencyUnitNotApplicableDto[],
      assessmentFile: {} as AssessmentFileWithTagsDto,
    };
  },
  computed: {
    noFilesSelected(): boolean {
      const validFiles = this.filesSelected.filter((f) => f.status === "Uploaded");
      return !(validFiles.length > 0);
    },
    selectedUnits(): number {
      return this.units.filter((x) => x.selected).length;
    },
    selectedElements(): number {
      const selectedElements = [] as ElementNode[];
      this.units.forEach((unit) => {
        selectedElements.push(...unit.elements.filter((x) => x.selected));
      });
      return selectedElements.length;
    },
    selectedCriteria(): number {
      const selectedCriteria = [] as CriteriaNode[];
      this.units.forEach((unit) => {
        unit.elements.forEach((element) => {
          selectedCriteria.push(...element.criteria.filter((x) => x.selected));
        });
      });
      return selectedCriteria.length;
    },
    noCriteriaSelected(): boolean {
      return !(this.selectedUnits + this.selectedElements + this.selectedCriteria);
    },
    noDescription(): boolean {
      return !(this.assessmentFile!.description);
    },
    isEdit(): boolean {
      return this.assessmentFileId != null;
    },
  },
  methods: {
    maskName,
    getTreeList,
    handleError,
    async getCompetencyUnitsNotApplicable() {
      if (featureEnabled(FeatureName.CompetencyUnitsNotApplicable)) {
        this.competencyUnitsNotApplicable = await assessmentService.getCompetencyUnitsNotApplicable(
            this.userId!,
            this.companyRoleId
        );
      }
    },
    maskFileName() {
      if (this.assessmentFile) {
        this.assessmentFile.originalFileName = this.maskName(this.assessmentFile.originalFileName);
      }
    },
    async shown() {
      this.activeTab = 0;
      if (this.$refs.fileUploader) {
        this.$refs.fileUploader.files = [];
      }
      if (this.$refs.observer) {
        this.$refs.observer.reset();
      }

      this.assessmentFile = {} as AssessmentFileWithTagsDto;
      this.units = [];
      this.unitTags = [];
      this.elementTags = [];
      this.criteriaTags = [];

      await this.getCompetencyUnitsNotApplicable();
      this.units = await this.getTreeList(this.competencyCycleId, this.competencyUnitsNotApplicable);
      if (this.isEdit) {
        this.assessmentFile = await evidenceLibraryService.getAssessmentFile(this.assessmentFileId)
        this.unitTags = this.assessmentFile.unitTags;
        this.elementTags = this.assessmentFile.elementTags;
        this.criteriaTags = this.assessmentFile.criteriaTags;

        this.setupSelectedItems();
        this.maskFileName();
      }
      this.loading = false;
    },
    fileInfo(files: ForgeFileStatus[]): FileTransferInformation {
      const blobFile = files[0];
      return {
        fileSizeBytes: blobFile.file.size,
        mimeType: blobFile.file.type,
        originalFilename: blobFile.customFileName != null ? blobFile.customFileName : blobFile.file.name
      } as FileTransferInformation;
    },
    async confirm() {
      const isValid = await this.$refs.observer.validate();
      if (this.noCriteriaSelected || (this.noFilesSelected && !this.isEdit) || !isValid) {
        return false;
      }
      if (this.isEdit) {
        await this.editFile();
      } else {
        await this.addFile();
      }
    },
    async addFile() {
      const files = this.filesSelected.filter((f) => f.status === "Uploaded");

      const assessmentFileWithTags = {
        userId: this.userId,
        companyRoleId: this.companyRoleId,
        assessmentFileId: null,
        description: this.assessmentFile?.description,
        whatCouldYouHaveDoneDifferently: this.assessmentFile?.whatCouldYouHaveDoneDifferently,
        inconclusive: this.assessmentFile?.inconclusive,
        madeInconclusiveDateTime: this.assessmentFile?.madeInconclusiveDateTime,
        createdDateTime: null,
        isScratchpad: false,
        assessmentFileSourceId: 0,
        fileUploadName: files[0].blobFileName,
        suffix: null,
        unitTags: this.unitTags,
        elementTags: this.elementTags,
        criteriaTags: this.criteriaTags,
        fileTransferInformation: this.fileInfo(files)
      } as PostAssessmentFileWithTagsDto

      await evidenceLibraryService
          .addAssessmentFile(assessmentFileWithTags)
          .then((response) => {
            this.$bvModal.hide("uploadEvidenceModal");
            this.$emit("confirmedEdit");
          })
          .catch((error) => {
            this.$forgeToast("error", error.message);
          });
    },
    async editFile() {
      const assessmentFileWithTags = {
        assessmentFileId: this.assessmentFile?.assessmentFileId,
        originalFileName: this.assessmentFile?.originalFileName,
        description: this.assessmentFile?.description,
        whatCouldYouHaveDoneDifferently: this.assessmentFile?.whatCouldYouHaveDoneDifferently,
        inconclusive: this.assessmentFile?.inconclusive,
        madeInconclusiveDateTime: this.assessmentFile?.madeInconclusiveDateTime,
        unitTags: this.unitTags,
        elementTags: this.elementTags,
        criteriaTags: this.criteriaTags
      } as PutAssessmentFileWithTagsDto

      await evidenceLibraryService
          .updateAssessmentFile(this.assessmentFile!.assessmentFileId!, assessmentFileWithTags)
          .then((response) => {
            this.$bvModal.hide("editEvidenceModal");
            this.$emit("confirmedEdit");
          })
          .catch((error) => {
            this.$forgeToast("error", error.message);
          });
    },
    async getNewFileUrl(userEnteredFileName: string) {
      try {
        let response = null;
        response = await evidenceLibraryService.createAzureBlobFileUrl(userEnteredFileName);
        return [response.fileWriteUrl, response.blobFileName];
      } catch (error) {
        this.handleError(error);
      }
    },
    setupSelectedItems() {
      this.units.forEach((unit) => {
        unit.selected = this.assessmentFile?.unitTags?.find((x) => x.referenceId == unit.referenceId) != null;
        unit.elements.forEach((element) => {
          element.selected =
              this.assessmentFile?.elementTags?.find((x) => x.referenceId == element.referenceId) != null;
          element.criteria.forEach((criteria) => {
            criteria.selected =
                this.assessmentFile?.criteriaTags?.find((x) => x.referenceId == criteria.referenceId) != null;
          });
        });
      });
    },
    validateFileName(fileName: string) {
      if (fileName.length == 0) {
        return { errors: ["Enter name"], valid: false } as ValidationResult;
      }

      if (fileName.length >= 500) {
        return { errors: ["Enter name under 500 characters"], valid: false } as ValidationResult;
      }

      return { errors: [], valid: true } as ValidationResult;
    },
  },
});
</script>

<style lang="scss">
#uploadEvidenceModal {
  .modal-body {
    height: 650px;
  }
}

.vue-page .container.w-100 {
  max-width: 100%;
}

.file-container table td {
  vertical-align: middle !important;
}

.forge-file-input span {
  font-size: 16px;
}
</style>