bcgov/document-generation-showcase

View on GitHub
app/frontend/src/components/FileInput.vue

Summary

Maintainability
Test Coverage
F
0%
<template>
  <v-card class="file-input pa-2 my-2">
    <v-card-title>Document Generation Form</v-card-title>

    <v-card-text>
      <v-form ref="form" v-model="validFileInput">
        <v-row class="docgen-row">
          <v-col cols="12" lg="6">
            <h3 class="pb-3">STEP 1: Add your Template</h3>
            <v-card>
              <v-toolbar light flat>
                <v-tabs v-model="templateTab">
                  <v-tab>Template Upload</v-tab>
                  <v-tab>Template Builder</v-tab>
                </v-tabs>
              </v-toolbar>
              <v-card-text>
                <v-tabs-items v-model="templateTab">
                  <v-tab-item>
                    <v-file-input
                      counter
                      :clearable="true"
                      label="Upload template file"
                      hint="See below for sample templates and supported formats"
                      persistent-hint
                      prepend-icon="attachment"
                      required
                      mandatory
                      :rules="this.templateTab === 0 ? notEmpty : []"
                      show-size
                      v-model="form.files"
                    />
                  </v-tab-item>
                  <v-tab-item>
                    <p>
                      Type in your Template contents, for example: 'Welcome
                      {d.firstName}!'.
                      <br />See
                      <a
                        href="https://carbone.io/documentation.html#substitutions"
                      >
                        Carbone documentation
                      </a>
                      for more details.
                    </p>
                    <v-textarea
                      class="template-builder"
                      auto-grow
                      hint="Enter your template text with 'contexts' (tokens like {d.firstName})"
                      rows="3"
                      :rules="templateBuilderRules"
                      v-model="form.templateContent"
                      solo
                      dense
                      single-line
                      outlined
                      flat
                    />
                  </v-tab-item>
                </v-tabs-items>
                <v-text-field
                  hint="(Optional) Desired output filename"
                  label="Output file name"
                  persistent-hint
                  v-model="form.outputFileName"
                />
                <v-checkbox
                  v-model="form.convertToPDF"
                  label="Convert to PDF"
                />
              </v-card-text>
            </v-card>
          </v-col>
          <v-col lg="6" cols="12">
            <h3 class="pb-3">STEP 2: Upload or create your Data File</h3>
            <v-spacer />
            <v-card>
              <v-toolbar light flat>
                <v-tabs v-model="contextTab">
                  <v-tab>Data File Upload</v-tab>
                  <v-tab>Data File Builder</v-tab>
                </v-tabs>
              </v-toolbar>

              <v-card-text>
                <v-tabs-items v-model="contextTab">
                  <v-tab-item>
                    <v-file-input
                      counter
                      :clearable="false"
                      hint="(Optional) JSON file with key-value pairs"
                      label="Upload data file"
                      persistent-hint
                      prepend-icon="attachment"
                      show-size
                      v-model="form.contextFiles"
                      class="mb-8"
                    />
                    <v-textarea
                      auto-grow
                      hint="JSON format for key-value pairs"
                      label="The JSON object with key-value pairs"
                      mandatory
                      required
                      rows="3"
                      :rules="contextsRules"
                      v-model="form.contexts"
                      dense
                      outlined
                      flat
                    />
                  </v-tab-item>
                  <v-tab-item>
                    <p>
                      Add key/value pairs for each of the contexts in your
                      template.
                    </p>
                    <JsonBuilder
                      @json-object="buildContexts"
                      ref="jsonBuilder"
                    />
                  </v-tab-item>
                </v-tabs-items>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>
      </v-form>
    </v-card-text>

    <v-card-actions>
      <v-tooltip top>
        <template v-slot:activator="{ on }">
          <v-btn
            outlined
            color="info"
            class="btn-file-input-reset"
            id="file-input-reset"
            @click="reset"
            v-on="on"
          >
            <v-icon :left="$vuetify.breakpoint.smAndUp">refresh</v-icon>
            <span v-if="$vuetify.breakpoint.smAndUp">Reset</span>
          </v-btn>
        </template>
        <span>Reset Form</span>
      </v-tooltip>
      <v-spacer />
      <v-tooltip top>
        <template v-slot:activator="{ on }">
          <v-btn
            color="primary"
            class="btn-file-input-submit"
            :disabled="!validFileInput"
            id="file-input-submit"
            :loading="loading"
            @click="generate"
            v-on="on"
          >
            <v-icon :left="$vuetify.breakpoint.smAndUp">save</v-icon>
            <span v-if="$vuetify.breakpoint.smAndUp">Submit</span>
          </v-btn>
        </template>
        <span>Submit to CDOGS and Download</span>
      </v-tooltip>
    </v-card-actions>

    <v-snackbar v-model="snack" :timeout="3000" :color="snackColor">
      <div class="d-flex align-center">
        <div class="mr-auto">
          {{ snackText }}
          <ul v-if="snackErrorList && snackErrorList.length">
            <li v-for="item in snackErrorList" :key="item.message">
              {{ item.message }}
            </li>
          </ul>
        </div>
        <v-btn text @click="snack = false">
          <v-icon>close</v-icon>
        </v-btn>
      </div>
    </v-snackbar>
  </v-card>
</template>

<script>
import JsonBuilder from '@/components/JsonBuilder.vue';

export default {
  name: 'fileInput',
  components: {
    JsonBuilder
  },
  computed: {
    contextFiles() {
      return this.form.contextFiles;
    },
    files() {
      return this.form.files;
    }
  },
  data() {
    return {
      contextsRules: [
        v => !!v || 'Cannot be empty',
        v => {
          try {
            return !!JSON.parse(v);
          } catch (e) {
            return 'Must be valid JSON';
          }
        },
        v => {
          try {
            JSON.parse(v);
            return true;
          } catch (e) {
            return 'Must be an JSON object';
          }
        },
        v => {
          const o = JSON.parse(v); // this should not fail due to earlier rules.
          if (Array.isArray(o)) {
            if (!o.length) return 'Array must have at least one element';
          }
          return true;
        }
      ],

      templateBuilderRules: [
        v =>
          !RegExp(/^.*?{(?!.*?})[^}]*$|^[^{\r\n]*}.*?$/).test(v) ||
          'Contexts should be enclosed by curly braces'
      ],
      templateTab: null,
      contextTab: null,
      form: {
        contexts: '{}',
        contextFiles: null,
        convertToPDF: null,
        files: null,
        templateContent: 'Hello {d.firstName} {d.lastName}!',
        contentFileType: null,
        outputFileName: '',
        outputFileType: null
      },
      loading: false,
      notEmpty: [v => !!v || 'Cannot be empty'],
      snack: false,
      snackColor: '',
      snackText: '',
      snackErrorList: [],
      validFileInput: null
    };
  },
  methods: {
    buildContexts(obj) {
      this.form.contextFiles = null;
      this.updateContexts(obj);
    },
    createBody(
      contexts,
      content,
      contentFileType,
      outputFileName,
      outputFileType
    ) {
      return {
        data: contexts,
        options: {
          reportName: outputFileName,
          convertTo: outputFileType,
          overwrite: true
        },
        template: {
          content: content,
          encodingType: 'base64',
          fileType: contentFileType
        }
      };
    },
    createDownload(blob, filename = undefined) {
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.style.display = 'none';
      a.href = url;
      a.download = filename;
      a.click();
      window.URL.revokeObjectURL(url);
      a.remove();
    },
    getDispositionFilename(disposition) {
      let filename = undefined;
      if (disposition) {
        filename = disposition.substring(disposition.indexOf('filename=') + 9);
      }
      return filename;
    },
    notifyError(errMsg, errors) {
      this.snack = true;
      this.snackColor = 'error';
      this.snackText = errMsg;
      this.snackErrorList = errors && Array.isArray(errors) ? errors : [];
    },
    notifyInfo(infoMsg) {
      this.snack = true;
      this.snackColor = 'info';
      this.snackText = infoMsg;
    },
    notifySuccess(msg) {
      this.snack = true;
      this.snackColor = 'success';
      this.snackText = msg;
    },
    async parseContextFiles() {
      try {
        if (this.form.contextFiles && this.form.contextFiles instanceof File) {
          // Parse Contents
          const content = await this.toTextObject(this.form.contextFiles);
          this.updateContexts(JSON.parse(content));
          this.notifySuccess('Parsed successfully');
        }
      } catch (e) {
        console.error(e); // eslint-disable-line no-console
        this.notifyError(e.message);
      }
    },
    reset() {
      // Reset all values to starting null
      Object.keys(this.form).forEach(key => {
        this.form[key] = null;
      });
      this.form.contexts = '{}';
      this.form.outputFileName = '';
      // clear json builder items
      if (this.$refs.jsonBuilder) this.$refs.jsonBuilder.reset();
      // Reset validation results
      this.$refs.form.resetValidation();

      this.notifyInfo('Form reset');
    },
    splitFileName(filename = undefined) {
      let name = undefined;
      let extension = undefined;

      if (filename) {
        const filenameArray = filename.split('.');
        name = filenameArray.slice(0, -1).join('.');
        extension = filenameArray.slice(-1).join('.');
      }

      return { name, extension };
    },
    fileToBase64(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => resolve(reader.result.replace(/^.*,/, ''));
        reader.onerror = error => reject(error);
      });
    },
    textToBase64(text) {
      return btoa(text);
    },
    toTextObject(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsText(file);
        reader.onload = () => resolve(reader.result);
        reader.onerror = error => reject(error);
      });
    },
    updateContexts(obj) {
      try {
        // create a JSON object of context(s)
        this.form.contexts = JSON.stringify(obj);
      } catch (e) {
        console.error(e, obj); // eslint-disable-line no-console
      }
    },
    async generate() {
      try {
        this.loading = true;
        let content = '';
        let contentFileType = '';
        let outputFileName = '';
        let outputFileType = '';
        let parsedContexts = '';

        parsedContexts = JSON.parse(this.form.contexts);

        // if uploading template file (tab is visible)
        if (this.templateTab === 0) {
          if (this.form.files && this.form.files instanceof File) {
            content = await this.fileToBase64(this.form.files);
            contentFileType = this.form.contentFileType;
          }
        }
        // else using template builder
        else {
          content = await this.textToBase64(this.form.templateContent);
          contentFileType = 'txt';
        }

        //if output file name field has a file extension, we need to separate out the name from the extension for conversion
        // may have templates in the output file name, so only look for separator AFTER templates...
        const _postTemplatesOutputFileName = this.form.outputFileName.substring(
          this.form.outputFileName.lastIndexOf('}') + 1
        );
        if (_postTemplatesOutputFileName.lastIndexOf('.') > -1) {
          // remove extension from output file name
          outputFileName = this.splitFileName(this.form.outputFileName)['name'];
          // use this extension as the output file type (or set as pdf if pdf checkbox was checked)
          outputFileType = this.form.convertToPDF
            ? 'pdf'
            : this.splitFileName(this.form.outputFileName)['extension'];
        }
        // else output file name contains no extension
        else {
          outputFileName = this.form.outputFileName;
          // output file type is empty (or set as pdf if pdf checkbox was checked)
          outputFileType = this.form.convertToPDF ? 'pdf' : '';
        }

        // if the outputFileName has a template string...
        // then it needs an extension in order to populate the template correctly.
        // it does not matter what the extension is, but outputFileName requires an extension for logic to kick in.
        // outputFileType still determines what type of file is generated.
        if (outputFileName.lastIndexOf('}') > -1) {
          outputFileName = `${outputFileName}.txt`;
        }

        // create payload to send to CDOGS API
        const body = this.createBody(
          parsedContexts,
          content,
          contentFileType,
          outputFileName,
          outputFileType
        );

        // Perform API Call
        const response = await this.$httpApi.post('/template/render', body, {
          responseType: 'arraybuffer', // Needed for binaries unless you want pain
          timeout: 30000 // Override default timeout as this call could take a while
        });

        // create file to download
        const filename = this.getDispositionFilename(
          response.headers['content-disposition']
        );

        const blob = new Blob([response.data], {
          type: 'attachment'
        });

        // Generate Temporary Download Link
        this.createDownload(blob, filename);
        this.notifySuccess('Submitted successfully');
      } catch (e) {
        console.error(e); // eslint-disable-line no-console
        if (e.response) {
          const data = new TextDecoder().decode(e.response.data);
          const parsed = JSON.parse(data);
          const errArray = parsed.status === 422 ? parsed.errors : undefined;
          this.notifyError(e, errArray);
        } else {
          this.notifyError(e);
        }
      } finally {
        this.loading = false;
      }
    }
  },
  watch: {
    contextFiles() {
      this.parseContextFiles();
    },
    files() {
      if (this.form.files && this.form.files instanceof File) {
        const { name, extension } = this.splitFileName(this.files.name);
        if (!this.form.outputFileName) {
          this.form.outputFileName = name;
        }
        this.form.contentFileType = extension;
      }
    }
  }
};
</script>