Select Git revision
UserProfile.vue

Hanna Führ authored and
Benedikt Heinrichs
committed
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
UserProfile.vue 18.87 KiB
<template>
<b-row>
<div class="col-sm-2" />
<div class="col-sm-8">
<b-form @submit.stop.prevent="onSubmit">
<!-- Personal Information -->
<CoscineHeadline
:headline="$t('page.userprofile.form.personalInformation.header')"
/>
<!-- Title -->
<coscine-form-group
label-for="title"
:label="
$t('page.userprofile.form.personalInformation.labels.titleLabel')
"
:is-loading="isLoading"
type="input"
>
<multiselect
id="title"
v-model="$v.form.title.$model"
:options="titles"
:multiple="false"
label="displayName"
track-by="displayName"
:show-labels="false"
:placeholder="
$t(
'page.userprofile.form.personalInformation.multiselect.placeholderTitle'
)
"
:select-label="
$t(
'page.userprofile.form.personalInformation.multiselect.selectEnter'
)
"
>
<span slot="noResult">
{{
$t(
"page.userprofile.form.personalInformation.multiselect.noResults"
)
}}
</span>
<span slot="noOptions">
{{
$t(
"page.userprofile.form.personalInformation.multiselect.noOptions"
)
}}
</span>
</multiselect>
</coscine-form-group>
<!-- Given Name -->
<coscine-form-group
:mandatory="true"
label-for="givenname"
:label="
$t(
'page.userprofile.form.personalInformation.labels.givenNameLabel'
)
"
:is-loading="isLoading"
type="input"
>
<b-form-input
id="givenname"
v-model="$v.form.givenname.$model"
:state="
$v.form.givenname.$dirty ? !$v.form.givenname.$invalid : null
"
:placeholder="
$t('page.userprofile.form.personalInformation.givenName')
"
/>
</coscine-form-group>
<!-- Surname -->
<coscine-form-group
:mandatory="true"
label-for="surname"
:label="
$t('page.userprofile.form.personalInformation.labels.surnameLabel')
"
:is-loading="isLoading"
type="input"
>
<b-form-input
id="surname"
v-model="$v.form.surname.$model"
:state="$v.form.surname.$dirty ? !$v.form.surname.$invalid : null"
:placeholder="
$t('page.userprofile.form.personalInformation.surname')
"
/>
</coscine-form-group>
<!-- Email -->
<coscine-form-group
:mandatory="true"
label-for="Email"
:label="
$t('page.userprofile.form.personalInformation.labels.emailLabel')
"
:is-loading="isLoading"
type="input"
>
<b-form-input
id="Email"
v-model="$v.form.emailAddress.$model"
:state="
$v.form.emailAddress.$dirty || !profileForm.form.emailAddress
? !$v.form.emailAddress.$invalid
: null
"
:placeholder="$t('page.userprofile.form.personalInformation.email')"
/>
<div id="emailHint">{{ emailHint }}</div>
</coscine-form-group>
<!-- Organization -->
<coscine-form-group
:mandatory="true"
label-for="organization"
:label="
$t(
'page.userprofile.form.personalInformation.labels.organizationLabel'
)
"
:is-loading="isLoading"
type="input"
>
<!-- Organization - External User -->
<multiselect
v-if="externalUser"
id="organization"
v-model="$v.form.organization.$model"
:options="ror"
:multiple="false"
:loading="loadingOrganizations"
:show-labels="false"
:placeholder="
$t(
'page.userprofile.form.personalInformation.multiselect.placeholderText'
)
"
@search-change="triggerFetchOptions"
>
<template slot="noOptions">
{{
$t(
"page.userprofile.form.personalInformation.multiselect.noOptionsOrganization"
)
}}
</template>
</multiselect>
<!-- Organization - Internal User -->
<multiselect
v-else
id="organization"
v-model="organizations"
:options="organizations"
:multiple="true"
:show-labels="false"
:placeholder="
$t('page.userprofile.form.personalInformation.organization')
"
:disabled="true"
/>
</coscine-form-group>
<!-- Institute -->
<coscine-form-group
:mandatory="true"
label-for="institute"
:label="
$t(
'page.userprofile.form.personalInformation.labels.instituteLabel'
)
"
:is-loading="isLoading"
type="input"
>
<!-- Institute - External User -->
<b-form-input
v-if="externalUser"
id="institute"
v-model="$v.form.institute.$model"
type="text"
:state="
$v.form.institute.$dirty ? !$v.form.institute.$invalid : null
"
:placeholder="
$t('page.userprofile.form.personalInformation.institute')
"
/>
<!-- Institute - Internal User -->
<multiselect
v-else
id="institute"
v-model="institutes"
:options="institutes"
:multiple="true"
:show-labels="false"
:placeholder="
$t('page.userprofile.form.personalInformation.institute')
"
:disabled="true"
/>
</coscine-form-group>
<!-- Discipline -->
<coscine-form-group
:mandatory="true"
label-for="Discipline"
:label="
$t(
'page.userprofile.form.personalInformation.labels.disciplineLabel'
)
"
:is-loading="isLoading"
type="input"
>
<multiselect
id="Discipline"
v-model="$v.form.disciplines.$model"
:options="disciplines"
:multiple="true"
:label="disciplineLabel"
:track-by="disciplineLabel"
:show-labels="false"
:placeholder="
$t(
'page.userprofile.form.personalInformation.multiselect.placeholderDiscipline'
)
"
>
<template slot="singleLabel" slot-scope="props">
{{ props.option[disciplineLabel] }}
</template>
<template slot="option" slot-scope="props">
{{ props.option[disciplineLabel] }}
</template>
</multiselect>
</coscine-form-group>
<!-- Access Token -->
<div class="h-divider" />
<CoscineHeadline
:headline="$t('page.userprofile.form.accessToken.header')"
/>
<AccessToken />
<!-- User Preferences -->
<div class="h-divider" />
<CoscineHeadline
:headline="$t('page.userprofile.form.userPreferences.header')"
/>
<!-- Language -->
<coscine-form-group
:mandatory="true"
label-for="language"
:label="
$t('page.userprofile.form.userPreferences.labels.languageLabel')
"
:is-loading="isLoading"
type="button"
>
<b-form-radio-group
id="language"
class="bv-no-focus-ring"
:checked="
profileForm.form.language && profileForm.form.language.id
? profileForm.form.language.id
: false
"
:state="
profileForm.form.language && profileForm.form.language.id
? null
: false
"
:options="languages"
name="radios-stacked"
text-field="displayName"
value-field="id"
stacked
@change="($event) => ($v.form.language.$model = { id: $event })"
/>
</coscine-form-group>
<!-- Connected Accounts -->
<div class="h-divider" />
<CoscineHeadline
:headline="$t('page.userprofile.form.connectedAccounts.header')"
/>
<coscine-form-group
:mandatory="true"
:label="
$t('page.userprofile.form.connectedAccounts.labels.orcidLabel')
"
:is-loading="isLoading"
type="button"
>
<b-button
variant="secondary"
class="float-left"
name="orcidConnect"
:disabled="orcidConnected"
@click.prevent="clickConnect('orcid')"
>
{{
orcidConnected ? $t("buttons.connected") : $t("buttons.connect")
}}
</b-button>
</coscine-form-group>
<coscine-form-group
:mandatory="true"
:label="
$t('page.userprofile.form.connectedAccounts.labels.shibbolethLabel')
"
:is-loading="isLoading"
type="button"
>
<b-button
variant="secondary"
class="float-left"
name="shibbolethConnect"
:disabled="shibbolethConnected"
@click.prevent="clickConnect('shibboleth')"
>
{{
shibbolethConnected
? $t("buttons.connected")
: $t("buttons.connect")
}}
</b-button>
</coscine-form-group>
<!-- Save Button -->
<coscine-form-group>
<b-button
ref="saveBtn"
type="submit"
variant="primary"
class="float-right"
name="save"
:disabled="$v.form.$invalid || savingProfile || !$v.form.$anyDirty"
@click.prevent="clickSave"
>
{{ $t("buttons.save") }}
</b-button>
</coscine-form-group>
</b-form>
</div>
<div class="col-sm-2" />
</b-row>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue-demi";
import { required, email } from "@vuelidate/validators";
import { useVuelidate, Validation } from "@vuelidate/core";
import AccessToken from "./components/AccessToken.vue";
import "@/plugins/deprecated/vue-multiselect";
// import the main store
import useMainStore from "@/store/index";
// import the store for current module
import useUserStore from "../store";
import type {
ContactChangeObject,
DisciplineObject,
LanguageObject,
TitleObject,
UserObject,
} from "@coscine/api-client/dist/types/Coscine.Api.User";
import type { OrganizationObject } from "@coscine/api-client/dist/types/Coscine.Api.Project";
import useNotificationStore from "@/store/notification";
export default defineComponent({
components: { AccessToken },
setup() {
const mainStore = useMainStore();
const userStore = useUserStore();
const notificationStore = useNotificationStore();
/*
Definition of the validation rules and initial state
will enable proper typings in the code
*/
const profileForm = reactive({
form: {
title: "",
givenname: "",
surname: "",
emailAddress: "",
organization: "",
institute: "",
disciplines: [] as DisciplineObject[],
language: {} as LanguageObject,
} as UserObject,
});
const profileRules = {
form: {
title: {},
givenname: { required },
surname: { required },
emailAddress: { email, required },
organization: { required },
institute: { required },
disciplines: { required },
language: { id: {} },
},
};
const $v = useVuelidate(profileRules, profileForm) as Validation<
typeof profileRules
>;
return { mainStore, userStore, notificationStore, $v, profileForm };
},
data() {
return {
isLoading: false,
savingProfile: false,
currentUserComponent: "UserProfileComponent",
disciplineLabel: "displayNameEn",
selectedExternalOrganization: null as Record<string, unknown> | null,
queryTimer: 0,
loadingOrganizations: false,
};
},
computed: {
contactChange(): null | ContactChangeObject[] {
return this.userStore.userProfile.contactChange;
},
disciplines(): null | DisciplineObject[] {
return this.userStore.userProfile.disciplines;
},
emailHint(): string {
if (!this.profileForm.form.emailAddress) {
return this.$t(
"page.userprofile.form.personalInformation.emailChange.noAddress"
).toString();
} else if (this.contactChange !== null && this.contactChange.length > 0) {
return this.$t(
"page.userprofile.form.personalInformation.emailChange.pendingConfirmation"
).toString();
}
return "";
},
externalUser(): boolean {
return !(
this.organizations &&
this.institutes &&
this.organizations.length > 0 &&
this.institutes.length > 0
);
},
institutes(): string[] {
if (this.userMemberships) {
return this.userMemberships
.filter((organization) => organization.url?.indexOf("#") !== -1) // If does contain "#" it's a sub level organization, otherwise top level
.map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings
.filter((n) => n); // Filter out empty strings, if any;;
}
return [];
},
languages(): null | LanguageObject[] {
return this.userStore.userProfile.languages;
},
userMemberships(): null | OrganizationObject[] {
return this.userStore.userProfile.userMemberships;
},
orcidConnected(): boolean {
if (this.user.externalAuthenticators) {
return this.user.externalAuthenticators.some(
(externalAuthenticator) =>
externalAuthenticator.displayName &&
externalAuthenticator.displayName.toLowerCase() === "orcid"
);
}
return false;
},
organizations(): string[] {
if (this.userMemberships) {
return this.userMemberships
.filter((organization) => !(organization.url?.indexOf("#") !== -1)) // If does contain "#" it's a sub level organization, otherwise top level
.map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings
.filter((n) => n); // Filter out empty strings, if any;
}
return [];
},
ror(): null | string[] {
const organizations = this.userStore.userProfile.organizations;
if (organizations) {
return organizations
.map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings
.filter((n) => n); // Filter out empty strings, if any
} else return null;
},
shibbolethConnected(): boolean {
if (this.user.externalAuthenticators) {
return this.user.externalAuthenticators.some(
(externalAuthenticator) =>
externalAuthenticator.displayName &&
externalAuthenticator.displayName.toLowerCase() === "shibboleth"
);
}
return false;
},
titles(): null | TitleObject[] {
return this.userStore.userProfile.titles;
},
user(): UserObject {
return this.userStore.user ? this.userStore.user : {};
},
},
watch: {
user() {
this.cloneUser();
},
},
created() {
this.cloneUser();
if (this.user.language === "en") {
this.disciplineLabel = "displayNameEn";
} else {
this.disciplineLabel = "displayNameDe";
}
},
methods: {
cloneUser() {
if (this.user !== null) {
Object.assign(this.profileForm.form, this.user);
}
},
async clickConnect(provider: string) {
await this.userStore.storeMergeToken(provider);
location.href = "/coscine/api/Coscine.Api.STS/account/logout";
},
async clickSave() {
this.$v.form.$touch();
if (!this.$v.form.$invalid && this.$v.form.$anyDirty) {
this.savingProfile = true;
// SECTION FOR SAVING ANY EMAIL CHANGES
// SIDE EFFECT: NO OTHER FIELD CHANGE CAN BE SAVED TOGETHER WITH A CHANGE OF THE EMAIL ADDRESS
// TODO
if (
this.profileForm.form.emailAddress &&
this.profileForm.form.emailAddress !== this.user.emailAddress
) {
await this.userStore.updateEmail(this.profileForm.form.emailAddress);
await Promise.all([
this.userStore.retrieveContactChange(),
this.userStore.retrieveUser(),
]);
this.notificationStore.postNotification({
title: this.$t(
"page.userprofile.form.personalInformation.emailChange.changedTitle"
).toString(),
body: this.$t(
"page.userprofile.form.personalInformation.emailChange.changedMessage"
).toString(),
});
}
// SECTION FOR SAVING THE REST OF THE FIELDS
const updatedUser = await this.userStore.updateUser(
this.profileForm.form
);
if (updatedUser) {
// On Success
this.$v.form.$reset();
this.notificationStore.postNotification({
title: this.$t("toast.onSave.success.title").toString(),
body: this.$t("toast.onSave.success.message").toString(),
});
} else {
// On Failure
this.notificationStore.postNotification({
title: this.$t("toast.onSave.failure.title").toString(),
body: this.$t("toast.onSave.failure.message").toString(),
variant: "danger",
});
}
await Promise.all([
this.userStore.retrieveContactChange(),
this.userStore.retrieveUser(),
]);
this.savingProfile = false;
}
},
triggerFetchOptions(search: string) {
clearTimeout(this.queryTimer);
if (search.length < 3) {
return;
}
this.queryTimer = window.setTimeout(
() => this.fetchOrganizationOptions(search),
1000
);
},
async fetchOrganizationOptions(search: string) {
this.loadingOrganizations = true;
await this.userStore.retrieveOrganizations(search);
this.loadingOrganizations = false;
},
},
});
</script>