Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
FormMetadata.vue 16.15 KiB
<template>
  <div>
    <!-- Project Metadata Section -->
    <CoscineHeadline :headline="$t('form.project.activatedImportFromParent')" />

    <!-- Copy Metadata -->
    <coscine-form-group
      v-if="parentProject"
      :mandatory="false"
      label-for="CopyData"
      :label="
        $t('form.project.copyMetadataLabel', {
          project: parentProject.projectName,
        })
      "
      :is-loading="isLoading"
      type="button"
      class="d-flex align-items-center"
    >
      <b-button
        id="project_copy_button"
        variant="secondary"
        size="sm"
        :disabled="disabled"
        @click.prevent="copyMetadataFromParent"
      >
        {{ $t("buttons.copyMetadata") }}</b-button
      >
    </coscine-form-group>

    <!-- Principal Investigators -->
    <coscine-form-group
      :mandatory="true"
      label-for="PrincipleInvestigators"
      :label="$t('form.project.projectPrincipleInvestigatorsLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <b-form-input
        id="PrincipleInvestigators"
        v-model="$v.projectForm.principleInvestigators.$model"
        :state="
          $v.projectForm.principleInvestigators.$dirty
            ? !$v.projectForm.principleInvestigators.$error
            : null
        "
        :placeholder="$t('form.project.projectPrincipleInvestigators')"
        :maxlength="$v.projectForm.principleInvestigators.maxLength.$params.max"
        required
        :disabled="disabled"
      />
      <div class="invalid-tooltip">
        {{
          $t("form.project.projectPrincipleInvestigatorsHelp", {
            maxLength:
              $v.projectForm.principleInvestigators.maxLength.$params.max,
          })
        }}
      </div>
    </coscine-form-group>

    <!-- Project Start -->
    <coscine-form-group
      :mandatory="true"
      label-for="StartDate"
      :label="$t('form.project.projectStartLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <b-form-datepicker
        id="StartDate"
        v-model="$v.projectForm.startDate.$model"
        :locale="$i18n.locale"
        :disabled="disabled"
        :state="
          $v.projectForm.startDate.$dirty
            ? !$v.projectForm.startDate.$error
            : null
        "
        required
        calendar-width="100%"
        start-weekday="1"
        :placeholder="$t('form.project.projectStart')"
      />
    </coscine-form-group>

    <!-- Project End -->
    <coscine-form-group
      :mandatory="true"
      label-for="EndDate"
      :label="$t('form.project.projectEndLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <b-form-datepicker
        id="EndDate"
        v-model="$v.projectForm.endDate.$model"
        :locale="$i18n.locale"
        :min="$v.projectForm.startDate.$model"
        :disabled="disabled"
        :state="
          $v.projectForm.endDate.$dirty ? !$v.projectForm.endDate.$error : null
        "
        required
        calendar-width="100%"
        start-weekday="1"
        :placeholder="$t('form.project.projectEnd')"
      />
    </coscine-form-group>

    <!-- Discipline -->
    <coscine-form-group
      :mandatory="true"
      label-for="Discipline"
      :label="$t('form.project.projectDisciplineLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <multiselect
        id="Discipline"
        v-model="$v.projectForm.disciplines.$model"
        :disabled="disabled"
        :options="disciplines"
        :multiple="true"
        :hide-selected="true"
        :label="disciplineLabel"
        :track-by="disciplineLabel"
        :placeholder="$t('form.project.projectDiscipline')"
        :show-labels="false"
      >
        <template #singleLabel(props)>
          <div :disabled="disabled">
            {{ props.option[disciplineLabel] }}
          </div>
        </template>
        <template #option(props)>
          <div :disabled="disabled">
            {{ props.option[disciplineLabel] }}
          </div>
        </template>
      </multiselect>
    </coscine-form-group>

    <!-- Participating Organizations -->
    <coscine-form-group
      :mandatory="true"
      label-for="Organization"
      :label="$t('form.project.projectOrganizationLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <multiselect
        id="Organization"
        v-model="$v.projectForm.organizations.$model"
        :disabled="disabled"
        :options="organizations"
        :multiple="true"
        :loading="isLoadingOrganizations"
        :hide-selected="true"
        label="displayName"
        :show-labels="false"
        track-by="url"
        :placeholder="$t('form.project.projectOrganization')"
        @search-change="retrieveOrganizations"
      >
        <template #singleLabel(props)>
          <div :disabled="disabled">
            {{ props.option.displayName }}
          </div>
        </template>
        <template #option(props)>
          <div :disabled="disabled">
            {{ props.option.displayName }}
          </div>
        </template>
        <template #noOptions>
          {{ $t("form.project.projectOrganizationNoOptions") }}
        </template>
        <template #noResult>
          {{ $t("form.project.projectOrganizationNoResult") }}
        </template>
      </multiselect>
    </coscine-form-group>

    <!-- Project Keywords -->
    <coscine-form-group
      label-for="Keywords"
      :label="$t('form.project.projectKeywordsLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <multiselect
        id="Keywords"
        v-model="selectedKeyword"
        :disabled="disabled"
        :options="selectedKeyword"
        :placeholder="$t('form.project.projectKeywordsPlaceholder')"
        :multiple="true"
        :taggable="true"
        :max="limitKeywords(selectedKeyword)"
        :tag-placeholder="$t('form.project.tagPlaceholder')"
        :show-labels="false"
        @tag="addTag"
        @remove="$v.projectForm.keywords.$touch()"
      >
        <template #maxElements>
          {{
            $t("form.project.projectKeywordsHelp", {
              maxLength: $v.projectForm.keywords.maxLength.$params.max,
            })
          }}
        </template>
        <template #noOptions>
          {{
            $t("form.project.projectKeywordsEmpty", {
              maxLength: $v.projectForm.keywords.maxLength.$params.max,
            })
          }}
        </template>
      </multiselect>
    </coscine-form-group>

    <!-- Visibility -->
    <coscine-form-group
      :mandatory="true"
      label-for="Visibility"
      :label="$t('form.project.projectMetadataVisibilityLabel')"
      :is-loading="isLoading"
      type="input"
      :info="true"
    >
      <template #popover>
        {{ $t("form.project.projectMetadataVisibilityLabelPopover") }}
        <b-link
          :href="
            $t(
              'form.project.projectMetadataVisibilityLabelPopoverUrl'
            ).toString()
          "
          target="_blank"
          >{{ $t("default.help") }}
        </b-link>
      </template>

      <b-form-radio-group
        v-model="selectedVisibility"
        name="radios-stacked"
        :options="visibilities"
        text-field="displayName"
        value-field="id"
        stacked
        :disabled="disabled"
        @change="setVisibility(selectedVisibility)"
      />
    </coscine-form-group>

    <!-- Grant ID -->
    <coscine-form-group
      label-for="GrantId"
      :label="$t('form.project.projectGrantIdLabel')"
      :is-loading="isLoading"
      type="input"
    >
      <b-form-input
        id="GrantId"
        v-model="$v.projectForm.grantId.$model"
        :state="
          $v.projectForm.grantId.$dirty ? !$v.projectForm.grantId.$error : null
        "
        :placeholder="$t('form.project.projectGrantId')"
        :maxlength="$v.projectForm.grantId.maxLength.$params.max"
        required
        :disabled="disabled"
      />
      <div class="invalid-tooltip">
        {{
          $t("form.project.projectGrantIdHelp", {
            maxLength: $v.projectForm.grantId.maxLength.$params.max,
          })
        }}
      </div>
    </coscine-form-group>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType, reactive, ref } from "vue-demi";
import { required, maxLength } from "@vuelidate/validators";
import { useVuelidate, Validation, ValidationArgs } from "@vuelidate/core";
import moment from "moment";
import "@/plugins/deprecated/vue-multiselect";

import type {
  DisciplineObject,
  OrganizationObject,
  ProjectObject,
  VisibilityObject,
} from "@coscine/api-client/dist/types/Coscine.Api.Project";

// import the store for current module
import useProjectStore from "../../store";
// import the main store
import useMainStore from "@/store/index";

export default defineComponent({
  props: {
    value: {
      type: Object as PropType<ProjectObject>,
      required: true,
    },
    currentProject: {
      default: null,
      type: [Object, null] as PropType<ProjectObject | null>,
      required: false,
    },
    parentProject: {
      default: null,
      type: [Object, null] as PropType<ProjectObject | null>,
      required: false,
    },
    disabled: {
      default: false,
      type: Boolean,
    },
    isLoading: {
      default: false,
      type: Boolean,
    },
  },
  emits: {
    validation: (_: ValidationArgs) => null,
  },

  setup(props) {
    const mainStore = useMainStore();
    const projectStore = useProjectStore();

    /*
      Definition of the validation rules and initial state
      will enable proper typings in the code
    */
    const state = reactive({
      projectForm: ref(props.value),
    });
    const rules = {
      projectForm: {
        principleInvestigators: { required, maxLength: maxLength(500) },
        startDate: { required },
        endDate: { required },
        disciplines: { required },
        organizations: { required },
        keywords: { maxLength: maxLength(1000) },
        visibility: { required },
        grantId: { maxLength: maxLength(500) },
      },
    };
    const $v = useVuelidate(rules, state) as Validation<typeof rules>;

    return { mainStore, projectStore, $v };
  },

  data() {
    return {
      projectForm: this.value,
      isLoadingOrganizations: false,
      selectedKeyword: [] as Array<string>,
      selectedVisibility: "" as string | undefined,
      queryTimer: 0,
    };
  },

  computed: {
    organizations(): OrganizationObject[] {
      return (
        this.projectStore.organizations ??
        this.projectStore.defaultOrganizations
      );
    },
    visibilities(): VisibilityObject[] | null {
      return this.projectStore.visibilities;
    },
    disciplines(): DisciplineObject[] {
      return this.projectStore.disciplines ?? [];
    },
    disciplineLabel(): string {
      let locale = this.$root.$i18n.locale;
      locale = locale.charAt(0).toUpperCase() + locale.slice(1);
      return `displayName${locale}`;
    },
  },

  watch: {
    currentProject() {
      this.onProjectLoaded();
    },
    parentProject() {
      this.onParentProjectLoaded();
    },
    visibilities() {
      // Used in Project Create
      if (!this.currentProject) {
        this.setDefaultVisibility();
      }
    },
    selectedKeyword() {
      const delimiter = ";";
      let keywords = "";
      this.selectedKeyword.forEach((element) => {
        keywords += `${element}${delimiter}`;
      });
      if (keywords.charAt(keywords.length - 1) === delimiter) {
        keywords = keywords.substring(0, keywords.length - 1);
      }
      this.projectForm.keywords = keywords;
    },
    "projectForm.startDate"() {
      // Adjust the endDate to match startDate, if startDate is past endDate
      const start = moment(this.projectForm.startDate);
      const end = moment(this.projectForm.endDate);
      const difference = moment.duration(start.diff(end)).asDays();
      if (difference > 0) {
        this.projectForm.endDate = this.projectForm.startDate;
      }
    },
    "projectForm.organizations"() {
      this.getLabels();
    },
    projectForm: {
      handler() {
        this.$emit("validation", this.$v.projectForm);
      },
      deep: true,
    },
  },

  created() {
    this.onProjectLoaded();
    this.onParentProjectLoaded();
  },

  methods: {
    onProjectLoaded() {
      if (this.currentProject) {
        this.loadKeywords(this.currentProject);
        if (this.currentProject.visibility) {
          this.selectedVisibility = this.currentProject.visibility.id;
        }
        this.getLabels();
      } else {
        this.setDefaultVisibility();
      }
    },

    onParentProjectLoaded() {
      if (this.parentProject) {
        this.projectForm.parentId = this.parentProject.id;
      }
    },

    limitKeywords(values: []): number | null {
      // Should the max number of allowed characters is exceeded, prevent the addition of further tags
      if (this.$v.projectForm.keywords?.$invalid) {
        return values.length;
      } else {
        return null;
      }
    },

    addTag(newTag: string) {
      this.selectedKeyword.push(newTag);
      this.$v.projectForm.keywords?.$touch();
    },

    async retrieveOrganizations(search: string) {
      clearTimeout(this.queryTimer);
      // Replace the list of available organizations
      // with the default one after the search field was cleared
      if (search.trim() === "") {
        // See computed property organizations' definition
        this.projectStore.organizations = null;
      }
      // Fetch organizations based on the search query
      else {
        // Add delay to API organization retrieval
        this.queryTimer = window.setTimeout(async () => {
          this.isLoadingOrganizations = true;
          await this.projectStore.retrieveOrganizations(search);
          this.isLoadingOrganizations = false;
        }, 1000);
      }
    },

    copyMetadataFromParent() {
      if (this.parentProject) {
        // --- set b-skeleton while filling parent-project form ---
        this.fillWithProjectMetadata(this.parentProject);
        this.getLabels();
        this.$v.projectForm.$touch();
      }
    },

    fillWithProjectMetadata(project: ProjectObject) {
      this.projectForm.principleInvestigators = project.principleInvestigators;
      this.projectForm.startDate = project.startDate;
      this.projectForm.endDate = project.endDate;
      this.projectForm.disciplines = project.disciplines;
      this.projectForm.organizations = project.organizations;
      this.projectForm.keywords = project.keywords;
      this.projectForm.visibility = project.visibility;
      this.projectForm.grantId = project.grantId;

      // Update v-model for Keywords
      this.loadKeywords(project);
      // Update v-model for Visibility
      if (project.visibility) {
        this.selectedVisibility = project.visibility.id;
      }
    },

    async getLabels() {
      // Retrieves an organization's displayName
      /* This method is required because the Organizations are delivered by the Database ProjectModel like so:
          displayName:"https://ror.org/04xfq0f34"
          url:"https://ror.org/04xfq0f34"
      */
      this.isLoadingOrganizations = true;
      if (this.projectForm.organizations) {
        for (const org of this.projectForm.organizations) {
          if (org.url) {
            const result = await this.projectStore.getOrganizationByURL(
              org.url
            );
            if (result) {
              for (const entry of result) {
                org.displayName = entry.displayName;
              }
            }
          }
        }
      }
      this.isLoadingOrganizations = false;
    },

    loadKeywords(project: ProjectObject) {
      this.selectedKeyword = project.keywords
        ? project.keywords.split(";")
        : [];
    },

    setVisibility(id: string) {
      if (this.visibilities) {
        const visibility = this.visibilities.find(
          (entry) => entry.id === id
        ) as VisibilityObject;
        this.projectForm.visibility = visibility;
        this.$v.projectForm.visibility?.$touch();
      }
    },

    setDefaultVisibility() {
      if (this.visibilities) {
        const visibility = this.visibilities.find(
          (entry) => entry.displayName === "Project Members"
        ) as VisibilityObject;
        this.selectedVisibility = visibility.id;
        this.projectForm.visibility = visibility;
      }
    },
  },
});
</script>

<style></style>