Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 2.11.0-privacyPolicy
  • APIv2
  • Docs/Setup
  • Experiment/fix-debugging
  • Experimental/Heinrichs-cypress
  • Feature/xxxx-turnOffDataPub
  • Fix/xxxx-ToS400Error
  • Fix/xxxx-migrateLogin
  • Fix/xxxx-tokenUploadButton
  • Hotfix/0038-correctDownload
  • Hotfix/1917-PublicFilesVisibility
  • Hotfix/1963-fixOrganizationField
  • Hotfix/2015-PublicFilesVisibility
  • Hotfix/2130-uiv2ContactChange
  • Hotfix/2144-invitationCall
  • Hotfix/2150-fixUpload
  • Hotfix/2160-userOrgsInst
  • Hotfix/2190-requiredFieldsForUserProfile
  • Hotfix/2196-RCVTableTranslation
  • Hotfix/2212-fixFiles
  • Hotfix/2226-userProfileSaveButton
  • Hotfix/2232-dependencyFix
  • Hotfix/2233-fixMe
  • Hotfix/2258-saveButtonWorksAsExpected
  • Hotfix/2296-selectedValuesNotReturned
  • Hotfix/2308-defaultLicense
  • Hotfix/2335-fixingSearchRCV
  • Hotfix/2353-dropShape
  • Hotfix/2370-fixDeleteButton
  • Hotfix/2378-linkedFix
  • Hotfix/2379-filesDragAndDrop
  • Hotfix/2382-guestStillBuggy
  • Hotfix/2384-guestsAndLinked
  • Hotfix/2427-adminTrouble
  • Hotfix/2459-EncodingPath
  • Hotfix/2465-orcidLink
  • Hotfix/2465-orcidLink-v1.25.1
  • Hotfix/2504-formGen
  • Hotfix/2541-resCreate
  • Hotfix/2601-correctMetadataIdentity
  • Hotfix/2611-feedback
  • Hotfix/2618-turtle
  • Hotfix/2681-validationErrors
  • Hotfix/2684-correctEncoding
  • Hotfix/2684-fixSubMetadata
  • Hotfix/2713-validateEntryName
  • Hotfix/2734-allowEmptyLicense
  • Hotfix/2765-encodingAgain
  • Hotfix/2852-adaptTextForToSUi
  • Hotfix/2853-optimizationV4
  • Hotfix/2943-reloadingResources
  • Hotfix/2943-searchHighlighting
  • Hotfix/2957-styleAndUpgrade
  • Hotfix/2971-fixTextInDataPub
  • Hotfix/2989-cookieLength
  • Hotfix/662-keepSidebarExpanded
  • Hotfix/xxxx-correctLinking
  • Hotfix/xxxx-folderRecursive
  • Hotfix/xxxx-fullscreenCss
  • Hotfix/xxxx-homepageDisplay
  • Hotfix/xxxx-liveReleaseFixes
  • Hotfix/xxxx-partnerProjects
  • Hotfix/xxxx-workingFileIndex
  • Issue/1782-structualDataIntegration
  • Issue/1792-newMetadataStructure
  • Issue/1822-coscineUIv2App
  • Issue/1824-componentsUIv2
  • Issue/1824-routerAdditions
  • Issue/1825-codeQualityPipelines
  • Issue/1833-newLogin
  • Issue/1843-multipleFilesValidation
  • Issue/1860-searchScoping
  • Issue/1861-searchMetadata
  • Issue/1862-searchFacets
  • Issue/1863-paginationForSearch
  • Issue/1926-userProfile
  • Issue/1927-projectAppMigration
  • Issue/1928-sidebarmenuAddition
  • Issue/1929-vuexToPinia
  • Issue/1938-internalHandling
  • Issue/1951-quotaImplementation
  • Issue/1953-owlImports
  • Issue/1957-resourceAppMigration
  • Issue/1957-resourceAppMigrationNew
  • Issue/1962-SearchAppUI2
  • Issue/1964-tokenExpiryUIv2
  • Issue/1965-userListMigration
  • Issue/1970-breadcrumbs
  • Issue/1971-projectEditCreateMigration
  • Issue/1972-homeDepot
  • Issue/1974-shibbolethLogout
  • Issue/1976-resouceCreationVaildEmail
  • Issue/1979-supportAdminUIv2Migration
  • Issue/1980-userManagement
  • Issue/1985-adaptSidebar
  • Issue/2002-migrateResourceCreate
  • Issue/2003-resourceSettings
  • Issue/2008-quotaManagement
  • Issue/2011-pathConfig
  • Issue/2016-BannerMigration
  • 1.28.0-pilot
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.10.3
  • v1.11.0
  • v1.11.1
  • v1.11.2
  • v1.11.3
  • v1.11.4
  • v1.11.5
  • v1.11.6
  • v1.11.7
  • v1.12.0
  • v1.13.0
  • v1.14.0
  • v1.14.1
  • v1.14.2
  • v1.14.3
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.16.1
  • v1.16.2
  • v1.16.3
  • v1.17.0
  • v1.17.1
  • v1.17.2
  • v1.18.0
  • v1.18.1
  • v1.19.0
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.20.4
  • v1.20.5
  • v1.21.0
  • v1.22.0
  • v1.22.1
  • v1.22.2
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.23.3
  • v1.23.4
  • v1.23.5
  • v1.23.6
  • v1.23.6-patch-2417-2427
  • v1.24.0
  • v1.24.1
  • v1.25.0
  • v1.25.1
  • v1.26.0
  • v1.26.1
  • v1.27.0
  • v1.27.1
  • v1.27.1-pilot
  • v1.28.0
  • v1.29.0
  • v1.29.1
  • v1.29.2
  • v1.3.0
  • v1.30.0
  • v1.30.1
  • v1.30.2
  • v1.31.0
  • v1.32.0
  • v1.4.0
  • v1.4.1
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.6.2
  • v1.7.0
  • v1.8.0
  • v1.8.1
  • v1.8.2
  • v1.9.0
  • v2.0.0
  • v2.1.0
  • v2.10.0
  • v2.10.1
  • v2.11.0
  • v2.12.0
  • v2.12.1
  • v2.12.2
  • v2.12.3
  • v2.12.4
  • v2.12.5
  • v2.13.0
  • v2.13.1
  • v2.13.2
  • v2.13.3
  • v2.13.4
  • v2.14.0
  • v2.15.0
200 results

Target

Select target project
  • coscine/frontend/apps/ui
1 result
Select Git revision
  • 2.11.0-privacyPolicy
  • APIv2
  • Docs/Setup
  • Experiment/fix-debugging
  • Experimental/Heinrichs-cypress
  • Feature/xxxx-turnOffDataPub
  • Fix/xxxx-ToS400Error
  • Fix/xxxx-migrateLogin
  • Fix/xxxx-tokenUploadButton
  • Hotfix/0038-correctDownload
  • Hotfix/1917-PublicFilesVisibility
  • Hotfix/1963-fixOrganizationField
  • Hotfix/2015-PublicFilesVisibility
  • Hotfix/2130-uiv2ContactChange
  • Hotfix/2144-invitationCall
  • Hotfix/2150-fixUpload
  • Hotfix/2160-userOrgsInst
  • Hotfix/2190-requiredFieldsForUserProfile
  • Hotfix/2196-RCVTableTranslation
  • Hotfix/2212-fixFiles
  • Hotfix/2226-userProfileSaveButton
  • Hotfix/2232-dependencyFix
  • Hotfix/2233-fixMe
  • Hotfix/2258-saveButtonWorksAsExpected
  • Hotfix/2296-selectedValuesNotReturned
  • Hotfix/2308-defaultLicense
  • Hotfix/2335-fixingSearchRCV
  • Hotfix/2353-dropShape
  • Hotfix/2370-fixDeleteButton
  • Hotfix/2378-linkedFix
  • Hotfix/2379-filesDragAndDrop
  • Hotfix/2382-guestStillBuggy
  • Hotfix/2384-guestsAndLinked
  • Hotfix/2427-adminTrouble
  • Hotfix/2459-EncodingPath
  • Hotfix/2465-orcidLink
  • Hotfix/2465-orcidLink-v1.25.1
  • Hotfix/2504-formGen
  • Hotfix/2541-resCreate
  • Hotfix/2601-correctMetadataIdentity
  • Hotfix/2611-feedback
  • Hotfix/2618-turtle
  • Hotfix/2681-validationErrors
  • Hotfix/2684-correctEncoding
  • Hotfix/2684-fixSubMetadata
  • Hotfix/2713-validateEntryName
  • Hotfix/2734-allowEmptyLicense
  • Hotfix/2765-encodingAgain
  • Hotfix/2852-adaptTextForToSUi
  • Hotfix/2853-optimizationV4
  • Hotfix/2943-reloadingResources
  • Hotfix/2943-searchHighlighting
  • Hotfix/2957-styleAndUpgrade
  • Hotfix/2971-fixTextInDataPub
  • Hotfix/2989-cookieLength
  • Hotfix/662-keepSidebarExpanded
  • Hotfix/xxxx-correctLinking
  • Hotfix/xxxx-folderRecursive
  • Hotfix/xxxx-fullscreenCss
  • Hotfix/xxxx-homepageDisplay
  • Hotfix/xxxx-liveReleaseFixes
  • Hotfix/xxxx-partnerProjects
  • Hotfix/xxxx-workingFileIndex
  • Issue/1782-structualDataIntegration
  • Issue/1792-newMetadataStructure
  • Issue/1822-coscineUIv2App
  • Issue/1824-componentsUIv2
  • Issue/1824-routerAdditions
  • Issue/1825-codeQualityPipelines
  • Issue/1833-newLogin
  • Issue/1843-multipleFilesValidation
  • Issue/1860-searchScoping
  • Issue/1861-searchMetadata
  • Issue/1862-searchFacets
  • Issue/1863-paginationForSearch
  • Issue/1926-userProfile
  • Issue/1927-projectAppMigration
  • Issue/1928-sidebarmenuAddition
  • Issue/1929-vuexToPinia
  • Issue/1938-internalHandling
  • Issue/1951-quotaImplementation
  • Issue/1953-owlImports
  • Issue/1957-resourceAppMigration
  • Issue/1957-resourceAppMigrationNew
  • Issue/1962-SearchAppUI2
  • Issue/1964-tokenExpiryUIv2
  • Issue/1965-userListMigration
  • Issue/1970-breadcrumbs
  • Issue/1971-projectEditCreateMigration
  • Issue/1972-homeDepot
  • Issue/1974-shibbolethLogout
  • Issue/1976-resouceCreationVaildEmail
  • Issue/1979-supportAdminUIv2Migration
  • Issue/1980-userManagement
  • Issue/1985-adaptSidebar
  • Issue/2002-migrateResourceCreate
  • Issue/2003-resourceSettings
  • Issue/2008-quotaManagement
  • Issue/2011-pathConfig
  • Issue/2016-BannerMigration
  • 1.28.0-pilot
  • v1.0.0
  • v1.1.0
  • v1.10.0
  • v1.10.1
  • v1.10.2
  • v1.10.3
  • v1.11.0
  • v1.11.1
  • v1.11.2
  • v1.11.3
  • v1.11.4
  • v1.11.5
  • v1.11.6
  • v1.11.7
  • v1.12.0
  • v1.13.0
  • v1.14.0
  • v1.14.1
  • v1.14.2
  • v1.14.3
  • v1.15.0
  • v1.15.1
  • v1.16.0
  • v1.16.1
  • v1.16.2
  • v1.16.3
  • v1.17.0
  • v1.17.1
  • v1.17.2
  • v1.18.0
  • v1.18.1
  • v1.19.0
  • v1.2.0
  • v1.20.0
  • v1.20.1
  • v1.20.2
  • v1.20.3
  • v1.20.4
  • v1.20.5
  • v1.21.0
  • v1.22.0
  • v1.22.1
  • v1.22.2
  • v1.23.0
  • v1.23.1
  • v1.23.2
  • v1.23.3
  • v1.23.4
  • v1.23.5
  • v1.23.6
  • v1.23.6-patch-2417-2427
  • v1.24.0
  • v1.24.1
  • v1.25.0
  • v1.25.1
  • v1.26.0
  • v1.26.1
  • v1.27.0
  • v1.27.1
  • v1.27.1-pilot
  • v1.28.0
  • v1.29.0
  • v1.29.1
  • v1.29.2
  • v1.3.0
  • v1.30.0
  • v1.30.1
  • v1.30.2
  • v1.31.0
  • v1.32.0
  • v1.4.0
  • v1.4.1
  • v1.5.0
  • v1.6.0
  • v1.6.1
  • v1.6.2
  • v1.7.0
  • v1.8.0
  • v1.8.1
  • v1.8.2
  • v1.9.0
  • v2.0.0
  • v2.1.0
  • v2.10.0
  • v2.10.1
  • v2.11.0
  • v2.12.0
  • v2.12.1
  • v2.12.2
  • v2.12.3
  • v2.12.4
  • v2.12.5
  • v2.13.0
  • v2.13.1
  • v2.13.2
  • v2.13.3
  • v2.13.4
  • v2.14.0
  • v2.15.0
200 results
Show changes
Showing
with 969 additions and 696 deletions
import { MapperConfiguration, MappingPair } from "@dynamic-mapper/mapper";
import type {
DisciplineDto,
DisciplineForUserManipulationDto,
LanguageDto,
LanguageForUserManipulationDto,
ProjectRoleUserDto,
PublicUserDto,
TitleDto,
TitleForUserManipulationDto,
UserDto,
UserForUpdateDto,
} from "@coscine/api-client/dist/types/Coscine.Api";
export const PublicUserDto2ProjectRoleUserDto = new MappingPair<
PublicUserDto,
ProjectRoleUserDto
>();
export const UserDto2UserForUpdateDto = new MappingPair<
UserDto,
UserForUpdateDto
>();
export const TitleDto2TitleForUserManipulationDto = new MappingPair<
TitleDto,
TitleForUserManipulationDto
>();
export const DisciplineDto2DisciplineForUserManipulationDto = new MappingPair<
DisciplineDto,
DisciplineForUserManipulationDto
>();
export const LanguageDto2LanguageForUserManipulationDto = new MappingPair<
LanguageDto,
LanguageForUserManipulationDto
>();
const configuration = new MapperConfiguration((cfg) => {
cfg.createAutoMap(PublicUserDto2ProjectRoleUserDto, {
emailAddress: (opt) => opt.mapFrom((u) => u.email),
userId: (opt) => opt.mapFrom((u) => u.id),
firstName: (opt) => opt.mapFrom((u) => u.firstName),
lastName: (opt) => opt.mapFrom((u) => u.lastName),
});
cfg.createMap(UserDto2UserForUpdateDto, {
firstName: (opt) => opt.auto(),
lastName: (opt) => opt.auto(),
email: (opt) => opt.auto(),
title: (opt) =>
opt
.condition((dto) => undefined !== dto.title)
.mapFromUsing((dto) => dto.title, TitleDto2TitleForUserManipulationDto),
disciplines: (opt) =>
opt.mapFromUsing(
(dto) => dto.disciplines,
DisciplineDto2DisciplineForUserManipulationDto
),
language: (opt) =>
opt.mapFromUsing(
(dto) => dto.language,
LanguageDto2LanguageForUserManipulationDto
),
institute: (opt) =>
opt.mapFrom((dto) =>
dto.institutes?.length ? dto.institutes[0].name : ""
),
organization: (opt) =>
opt.mapFrom((dto) =>
dto.organizations?.length ? dto.organizations[0].name : null
),
});
cfg.createAutoMap(TitleDto2TitleForUserManipulationDto, {});
cfg.createAutoMap(DisciplineDto2DisciplineForUserManipulationDto, {});
cfg.createAutoMap(LanguageDto2LanguageForUserManipulationDto, {});
});
export const userMapper = configuration.createMapper();
...@@ -58,7 +58,7 @@ ...@@ -58,7 +58,7 @@
{{ {{
!row.item.isQuotaAvailable !row.item.isQuotaAvailable
? $t("default.none") ? $t("default.none")
: $t("page.admin.gb", { number: toGiB(row.item.maximum) }) : $t("default.gb", { number: toGiB(row.item.maximum) })
}} }}
</strong> </strong>
</template> </template>
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
{{ {{
!row.item.isQuotaAvailable !row.item.isQuotaAvailable
? $t("default.none").toString() ? $t("default.none").toString()
: $t("page.admin.gb", { : $t("default.gb", {
number: toGiB(row.item.allocated).toString(), number: toGiB(row.item.allocated).toString(),
}) })
}} }}
...@@ -81,7 +81,7 @@ ...@@ -81,7 +81,7 @@
{{ {{
!row.item.isQuotaAvailable !row.item.isQuotaAvailable
? $t("default.none") ? $t("default.none")
: $t("page.admin.gb", { : $t("default.gb", {
number: row.item.free.value, number: row.item.free.value,
}) })
}} }}
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
{{ {{
!row.item.isQuotaAvailable !row.item.isQuotaAvailable
? $t("default.none") ? $t("default.none")
: $t("page.admin.gb", { number: toGiB(row.item.totalReserved) }) : $t("default.gb", { number: toGiB(row.item.totalReserved) })
}} }}
</template> </template>
...@@ -173,7 +173,13 @@ import useResourceStore from "@/modules/resource/store"; ...@@ -173,7 +173,13 @@ import useResourceStore from "@/modules/resource/store";
import type { ExtendedProjectQuotaObject } from "../types"; import type { ExtendedProjectQuotaObject } from "../types";
import { isNumber } from "lodash"; import { isNumber } from "lodash";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import { FileUtil, QuotaUnits } from "@/modules/resource/utils/FileUtil"; import { FileUtil } from "@/modules/resource/utils/FileUtil";
import type {
QuotaDto,
ResourceTypeInformationDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
import { QuotaUnit } from "@/modules/resource/types";
import { adminMapper, QuotaDimObject2QuotaDto } from "@/mapping/admin";
export default defineComponent({ export default defineComponent({
setup() { setup() {
...@@ -203,15 +209,17 @@ export default defineComponent({ ...@@ -203,15 +209,17 @@ export default defineComponent({
} }
// Retrieve all project quotas (incl. for hidden resources) // Retrieve all project quotas (incl. for hidden resources)
const projectQuotas = this.project.quotas; const projectQuotas = this.project.quotas;
if (!projectQuotas || !this.resourceStore.resourceTypes) { // Get all resource types
const resourceTypes: ResourceTypeInformationDto[] | null | undefined =
this.resourceStore.resourceTypes;
if (!projectQuotas || !resourceTypes) {
return null; // In case resource types can't be resolved return null return null; // In case resource types can't be resolved return null
} }
// Get all resource types
const resourceTypes = this.resourceStore.resourceTypes;
return projectQuotas.map((entry) => { return projectQuotas.map((entry) => {
// Find corresponding entry // Find corresponding entry
const resourceType = resourceTypes.find( const resourceType = resourceTypes.find(
(q) => q.displayName === entry.resourceType (q) => q.specificType === entry.resourceType
); );
// Populate the extended project quota object // Populate the extended project quota object
const extended = { const extended = {
...@@ -227,9 +235,9 @@ export default defineComponent({ ...@@ -227,9 +235,9 @@ export default defineComponent({
? { ? {
value: value:
this.toGiB(entry.maximum) - this.toGiB(entry.allocated), this.toGiB(entry.maximum) - this.toGiB(entry.allocated),
unit: QuotaUnits.GibiBYTE, unit: QuotaUnit.GibiByte,
} }
: { unit: QuotaUnits.GibiBYTE, value: undefined }, : { unit: QuotaUnit.GibiByte, value: undefined },
} as ExtendedProjectQuotaObject; } as ExtendedProjectQuotaObject;
// For every resource type that has no quota available, set the value to be undefined // For every resource type that has no quota available, set the value to be undefined
if (!extended.isQuotaAvailable) { if (!extended.isQuotaAvailable) {
...@@ -316,7 +324,7 @@ export default defineComponent({ ...@@ -316,7 +324,7 @@ export default defineComponent({
created() { created() {
// Load list of all Resource Types if not present // Load list of all Resource Types if not present
if (this.resourceStore.resourceTypes === null) { if (this.resourceStore.resourceTypes === null) {
this.resourceStore.retrieveResourceTypes(); this.resourceStore.retrieveAllResourceTypesInformation();
} }
}, },
...@@ -383,13 +391,19 @@ export default defineComponent({ ...@@ -383,13 +391,19 @@ export default defineComponent({
); );
} else return false; } else return false;
}, },
/**
* Converts a quota value to GiB (Gibibytes).
* @param quota - The quota to be converted.
* @returns {number} - The quota value in GiB.
*/
toGiB(quota: QuotaDimObject): number { toGiB(quota: QuotaDimObject): number {
const quotaDto = adminMapper.map(QuotaDimObject2QuotaDto, quota);
// Almost all displayed values inside the html must be shown in GiB, making this method necessary to ensure that's always the case. // Almost all displayed values inside the html must be shown in GiB, making this method necessary to ensure that's always the case.
return FileUtil.convertCapacityUnits(quota, QuotaUnits.GibiBYTE); return FileUtil.convertCapacityUnits(quotaDto, QuotaUnit.GibiByte);
}, },
formatUsed(quota: QuotaDimObject): string { formatUsed(quota: QuotaDto): string {
return FileUtil.formatBytes( return FileUtil.formatBytes(
FileUtil.convertCapacityUnits(quota, QuotaUnits.BYTE) FileUtil.convertCapacityUnits(quota, QuotaUnit.Byte)
); );
}, },
}, },
......
...@@ -60,12 +60,14 @@ import { defineComponent } from "vue"; ...@@ -60,12 +60,14 @@ import { defineComponent } from "vue";
// import the store for current module // import the store for current module
import useLoginStore from "../store"; import useLoginStore from "../store";
import useUserStore from "@/modules/user/store";
export default defineComponent({ export default defineComponent({
setup() { setup() {
const loginStore = useLoginStore(); const loginStore = useLoginStore();
const userStore = useUserStore();
return { loginStore }; return { loginStore, userStore };
}, },
data() { data() {
...@@ -76,8 +78,8 @@ export default defineComponent({ ...@@ -76,8 +78,8 @@ export default defineComponent({
}, },
computed: { computed: {
areTosAccepted(): boolean | null { areTosAccepted(): boolean | null | undefined {
return this.loginStore.areTosAccepted; return this.userStore.user?.areToSAccepted;
}, },
}, },
...@@ -85,28 +87,24 @@ export default defineComponent({ ...@@ -85,28 +87,24 @@ export default defineComponent({
areTosAccepted() { areTosAccepted() {
this.isTosChecked = this.areTosAccepted ? true : false; this.isTosChecked = this.areTosAccepted ? true : false;
this.isPpChecked = this.areTosAccepted ? true : false; this.isPpChecked = this.areTosAccepted ? true : false;
// Navigate away from ToS page
if (this.areTosAccepted) {
this.$router.replace({ name: "home" });
}
}, },
}, },
async created() { async created() {
await this.retrieveCurrentTosVersion();
this.isTosChecked = this.areTosAccepted ? true : false; this.isTosChecked = this.areTosAccepted ? true : false;
this.isPpChecked = this.areTosAccepted ? true : false; this.isPpChecked = this.areTosAccepted ? true : false;
}, },
methods: { methods: {
async retrieveCurrentTosVersion() {
if (!this.loginStore.currentTosVersion) {
await this.loginStore.retrieveCurrentTosVersion();
}
if (!this.loginStore.tosAccepted) {
await this.loginStore.retrieveTosAcceptanceStatus();
}
},
async accept() { async accept() {
if (this.isTosChecked && this.isPpChecked) { if (this.isTosChecked && this.isPpChecked) {
await this.loginStore.acceptToS(); await this.loginStore.acceptToS();
window.location.href = "/"; await this.userStore.retrieveUser();
} }
}, },
cancel() { cancel() {
......
...@@ -8,11 +8,10 @@ import type { DFNAAIData, DFNAAIInstitution, LoginState } from "./types"; ...@@ -8,11 +8,10 @@ import type { DFNAAIData, DFNAAIInstitution, LoginState } from "./types";
import useMainStore from "@/store/index"; import useMainStore from "@/store/index";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import { useLocalStorage } from "@vueuse/core"; import { useLocalStorage } from "@vueuse/core";
import { AccountApi, TOSApi } from "@coscine/api-client"; import { AccountApi, ToSApi, UserApi } from "@coscine/api-client";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import axios from "axios"; import axios from "axios";
import type { LoginUrls } from "@coscine/api-client/dist/types/Coscine.Api.STS"; import type { LoginUrls } from "@coscine/api-client/dist/types/Coscine.Api.STS";
import type { Tos } from "@coscine/api-client/dist/types/Coscine.Api.User";
/* /*
Store variable name is "this.<id>Store" Store variable name is "this.<id>Store"
...@@ -28,7 +27,6 @@ export const useLoginStore = defineStore({ ...@@ -28,7 +27,6 @@ export const useLoginStore = defineStore({
*/ */
state: (): LoginState => ({ state: (): LoginState => ({
expiredSession: null, expiredSession: null,
tosAccepted: null,
currentTosVersion: null, currentTosVersion: null,
loginStoredData: useLocalStorage("coscine.login.storedData", ""), // TODO: Use Type DFNAAIInstitution from March 2023 loginStoredData: useLocalStorage("coscine.login.storedData", ""), // TODO: Use Type DFNAAIInstitution from March 2023
loginUrls: null, loginUrls: null,
...@@ -56,18 +54,6 @@ export const useLoginStore = defineStore({ ...@@ -56,18 +54,6 @@ export const useLoginStore = defineStore({
return false; return false;
} }
}, },
areTosAccepted(): boolean | null {
if (this.isLoggedIn) {
if (
this.tosAccepted &&
this.tosAccepted.version &&
this.currentTosVersion &&
this.tosAccepted.version.includes(this.currentTosVersion)
) {
return true;
} else return false;
} else return null;
},
institutions(): DFNAAIData[] { institutions(): DFNAAIData[] {
return Institutes as DFNAAIData[]; return Institutes as DFNAAIData[];
}, },
...@@ -156,22 +142,11 @@ export const useLoginStore = defineStore({ ...@@ -156,22 +142,11 @@ export const useLoginStore = defineStore({
} }
}, },
async retrieveTosAcceptanceStatus() {
const notificationStore = useNotificationStore();
try {
const apiResponse = await TOSApi.tOSAcceptedTOSVersion();
this.tosAccepted = apiResponse.data as Tos;
} catch (error) {
// Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError);
}
},
async retrieveCurrentTosVersion() { async retrieveCurrentTosVersion() {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
const apiResponse = await TOSApi.tOSGetCurrentTOSVersion(); const apiResponse = await ToSApi.getToS();
this.currentTosVersion = apiResponse.data + ""; this.currentTosVersion = apiResponse.data.data;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
...@@ -193,15 +168,15 @@ export const useLoginStore = defineStore({ ...@@ -193,15 +168,15 @@ export const useLoginStore = defineStore({
this.loginStoredData = JSON.stringify(institution); this.loginStoredData = JSON.stringify(institution);
}, },
async acceptToS() { async acceptToS(): Promise<boolean> {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
const apiResponse = await TOSApi.tOSAcceptCurrentTOSVersion(); await UserApi.acceptCurrentToS();
return apiResponse.data; return true;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
return ""; return false;
} }
}, },
}, },
......
import type { TermsOfServiceDto } from "@coscine/api-client/dist/types/Coscine.Api";
import { LoginUrls } from "@coscine/api-client/dist/types/Coscine.Api.STS"; import { LoginUrls } from "@coscine/api-client/dist/types/Coscine.Api.STS";
import type { Tos } from "@coscine/api-client/dist/types/Coscine.Api.User";
import { RemovableRef } from "@vueuse/core"; import { RemovableRef } from "@vueuse/core";
export interface LoginState { export interface LoginState {
...@@ -9,8 +9,7 @@ export interface LoginState { ...@@ -9,8 +9,7 @@ export interface LoginState {
-------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------
*/ */
expiredSession: boolean | null; expiredSession: boolean | null;
tosAccepted: Tos | null; currentTosVersion: TermsOfServiceDto | null | undefined;
currentTosVersion: string | null;
loginStoredData: RemovableRef<string>; loginStoredData: RemovableRef<string>;
loginUrls: LoginUrls | null; loginUrls: LoginUrls | null;
} }
......
...@@ -42,12 +42,12 @@ export default { ...@@ -42,12 +42,12 @@ export default {
yourMessageLabel: yourMessageLabel:
"@:(page.pid.form.yourMessage)@:(page.pid.form.labelSymbol)", "@:(page.pid.form.yourMessage)@:(page.pid.form.labelSymbol)",
sendCopy: "Kopie an mich senden", sendConfirmationEmail: "Bestätigungsmail an mich senden",
}, },
toastEmailSent: { toastEmailSent: {
title: "Nachricht wurde versendet", title: "Nachricht wurde versendet",
body: 'Ihre Nachricht wurde an den Ressourcenbesitzer gesendet. Wenn Sie "@:(page.pid.form.sendCopy)" ausgewählt haben, empfangen Sie ebenfalls eine Kopie der Nachricht.', body: 'Ihre Nachricht wurde an den Ressourcenbesitzer gesendet. Wenn Sie "@:(page.pid.form.sendConfirmationEmail)" ausgewählt haben, empfangen Sie ebenfalls eine Kopie der Nachricht.',
}, },
}, },
}, },
......
...@@ -42,12 +42,12 @@ export default { ...@@ -42,12 +42,12 @@ export default {
yourMessageLabel: yourMessageLabel:
"@:(page.pid.form.yourMessage)@:(page.pid.form.labelSymbol)", "@:(page.pid.form.yourMessage)@:(page.pid.form.labelSymbol)",
sendCopy: "Send me a copy", sendConfirmationEmail: "Email me a confirmation",
}, },
toastEmailSent: { toastEmailSent: {
title: "Message was sent successfully", title: "Message was sent successfully",
body: 'Your message was sent to the resource owner. If you have selected "@:(page.pid.form.sendCopy)", you will also receive the request via e-mail.', body: 'Your message was sent to the resource owner. If you have selected "@:(page.pid.form.sendConfirmationEmail)", you will also receive the request via e-mail.',
}, },
}, },
}, },
......
/* Testing imports */
import { createLocalVue, shallowMount, Wrapper } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import type Vue from "vue";
/* Vue i18n */
import i18n, { def } from "@/plugins/vue-i18n";
import { PidI18nMessages } from "@/modules/pid/i18n/index";
i18n.availableLocales.forEach((locale) => {
i18n.setLocaleMessage(locale, def[locale]); // default locale messages
i18n.mergeLocaleMessage(locale, PidI18nMessages[locale]); // append the locale messages for the component
});
/* Pinia */
import { PiniaVuePlugin } from "pinia";
/* Additional Dependencies */
/* Tested Component */
import Pid from "./Pid.vue";
import VueRouter from "vue-router";
import { routes } from "@/router";
/* Create a local Vue instance */
const localVue = createLocalVue();
localVue.use(PiniaVuePlugin);
localVue.use(VueRouter);
const router = new VueRouter({ routes: routes });
// Define the Vue instance type (computed properties)
interface PidComponent extends Vue {
pid: string | null;
}
describe("Pid.vue", () => {
let wrapper: Wrapper<PidComponent>;
beforeEach(() => {
wrapper = shallowMount(Pid as unknown as typeof Vue, {
pinia: createTestingPinia({
createSpy: vitest.fn,
}),
router,
i18n,
localVue,
}) as Wrapper<PidComponent>;
});
/* Describe Pre-initialization steps */
/* Testing the computed property 'pid' */
test("Computed property 'pid' should be set correctly from raw pid", async () => {
// Simulate navigation to a specific route with a query parameter 'pid'
await router.push({
name: "pid-page",
query: { pid: "11148/ee1572f5-b5ef-41b0-8144-9c3c41db77d9" },
});
// Make sure changes in the mocked route are reflected in the component
await wrapper.vm.$nextTick();
// Now check the computed property 'pid'
expect(wrapper.vm.pid).toBe("11148/ee1572f5-b5ef-41b0-8144-9c3c41db77d9");
});
/* Testing the computed property 'pid' */
test("Computed property 'pid' should be set correctly from 'handle.net' pid", async () => {
// Simulate navigation to a specific route with a query parameter 'pid'
await router.push({
name: "pid-page",
query: {
pid: "http://hdl.handle.net/11148/ee1572f5-b5ef-41b0-8144-9c3c41db77d9",
},
});
// Make sure changes in the mocked route are reflected in the component
await wrapper.vm.$nextTick();
// Now check the computed property 'pid'
expect(wrapper.vm.pid).toBe("11148/ee1572f5-b5ef-41b0-8144-9c3c41db77d9");
});
test("disables send email form when PID is invalid and enables it when PID is valid", async () => {
// Verify that the PID field is disabled
const persistentId = wrapper.get("#PersistentId");
expect(persistentId.attributes()["readonly"]).toBe("true");
// Simulate an invalid PID
wrapper.vm.$data.isPidValid = false;
await wrapper.vm.$nextTick();
// Verify that the send email form is disabled
let nameField = wrapper.get("#YourName");
expect(Boolean(nameField.attributes()["disabled"])).toBeTruthy();
let emailField = wrapper.get("#YourEmail");
expect(Boolean(emailField.attributes()["disabled"])).toBeTruthy();
let messageField = wrapper.get("#YourMessage");
expect(Boolean(messageField.attributes()["disabled"])).toBeTruthy();
// Simulate a valid PID
wrapper.vm.$data.isPidValid = true;
await wrapper.vm.$nextTick();
// Verify that the send email form is NOT disabled
nameField = wrapper.get("#YourName");
expect(Boolean(nameField.attributes()["disabled"])).toBeFalsy();
emailField = wrapper.get("#YourEmail");
expect(Boolean(emailField.attributes()["disabled"])).toBeFalsy();
messageField = wrapper.get("#YourMessage");
expect(Boolean(messageField.attributes()["disabled"])).toBeFalsy();
});
});
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
id="PersistentId" id="PersistentId"
v-model="pid" v-model="pid"
:readonly="true" :readonly="true"
:state="!isPidValid ? false : null" :state="isLoading ? null : isPidValid ? null : false"
/> />
<div class="invalid-tooltip">{{ $t("page.pid.noPid") }}</div> <div class="invalid-tooltip">{{ $t("page.pid.noPid") }}</div>
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
v-model="v$.form.name.$model" v-model="v$.form.name.$model"
:state="v$.form.name.$dirty ? !v$.form.name.$invalid : null" :state="v$.form.name.$dirty ? !v$.form.name.$invalid : null"
:placeholder="$t('page.pid.form.yourName')" :placeholder="$t('page.pid.form.yourName')"
:disabled="!pid" :disabled="!pid || !isPidValid"
required required
/> />
</CoscineFormGroup> </CoscineFormGroup>
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
v-model="v$.form.email.$model" v-model="v$.form.email.$model"
:state="v$.form.email.$dirty ? !v$.form.email.$invalid : null" :state="v$.form.email.$dirty ? !v$.form.email.$invalid : null"
:placeholder="$t('page.pid.form.yourEmailAddress')" :placeholder="$t('page.pid.form.yourEmailAddress')"
:disabled="!pid" :disabled="!pid || !isPidValid"
required required
/> />
</CoscineFormGroup> </CoscineFormGroup>
...@@ -109,15 +109,18 @@ ...@@ -109,15 +109,18 @@
v-model="v$.form.message.$model" v-model="v$.form.message.$model"
:state="v$.form.message.$dirty ? !v$.form.message.$invalid : null" :state="v$.form.message.$dirty ? !v$.form.message.$invalid : null"
:placeholder="$t('page.pid.form.yourMessage')" :placeholder="$t('page.pid.form.yourMessage')"
:disabled="!pid" :disabled="!pid || !isPidValid"
required required
/> />
</CoscineFormGroup> </CoscineFormGroup>
<!-- Your Message --> <!-- Your Message -->
<CoscineFormGroup :is-loading="isLoading" type="input"> <CoscineFormGroup :is-loading="isLoading" type="input">
<b-form-checkbox v-model="v$.form.sendCopy.$model"> <b-form-checkbox
{{ $t("page.pid.form.sendCopy") }} v-model="v$.form.sendConfirmationEmail.$model"
:disabled="!pid || !isPidValid"
>
{{ $t("page.pid.form.sendConfirmationEmail") }}
</b-form-checkbox> </b-form-checkbox>
</CoscineFormGroup> </CoscineFormGroup>
...@@ -127,7 +130,9 @@ ...@@ -127,7 +130,9 @@
type="submit" type="submit"
variant="primary" variant="primary"
class="float-right" class="float-right"
:disabled="v$.form.$invalid || !v$.form.$anyDirty || isLoading" :disabled="
v$.form.$invalid || !v$.form.$anyDirty || isLoading || !isPidValid
"
@click.prevent="clickSubmit" @click.prevent="clickSubmit"
> >
{{ $t("buttons.submit") }} {{ $t("buttons.submit") }}
...@@ -144,7 +149,7 @@ import usePidStore from "../store"; ...@@ -144,7 +149,7 @@ import usePidStore from "../store";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import { useVuelidate, type ValidationArgs } from "@vuelidate/core"; import { useVuelidate, type ValidationArgs } from "@vuelidate/core";
import { email, maxLength, required } from "@vuelidate/validators"; import { email, maxLength, required } from "@vuelidate/validators";
import type { MessageObject } from "@coscine/api-client/dist/types/Coscine.Api.Pid"; import type { PidEnquiryDto } from "@coscine/api-client/dist/types/Coscine.Api/api";
export default defineComponent({ export default defineComponent({
setup() { setup() {
...@@ -162,8 +167,8 @@ export default defineComponent({ ...@@ -162,8 +167,8 @@ export default defineComponent({
name: "", name: "",
email: "", email: "",
message: "", message: "",
sendCopy: true, sendConfirmationEmail: true,
} as MessageObject, } as PidEnquiryDto,
isLoading: false, isLoading: false,
isPidValid: null as boolean | null, isPidValid: null as boolean | null,
}; };
...@@ -184,19 +189,19 @@ export default defineComponent({ ...@@ -184,19 +189,19 @@ export default defineComponent({
name: { required }, name: { required },
email: { email, required }, email: { email, required },
message: { required, maxLength: maxLength(5000) }, message: { required, maxLength: maxLength(5000) },
sendCopy: {}, sendConfirmationEmail: { required },
} as ValidationArgs<MessageObject>, } as ValidationArgs<PidEnquiryDto>,
}; };
}, },
watch: { watch: {
async pid() { async pid() {
this.isPidValid = await this.validatePid(); await this.validatePid();
}, },
}, },
async created() { async created() {
this.isPidValid = await this.validatePid(); await this.validatePid();
}, },
methods: { methods: {
...@@ -206,23 +211,29 @@ export default defineComponent({ ...@@ -206,23 +211,29 @@ export default defineComponent({
this.$root.$emit("bv::show::tooltip", "copyPidTooltip"); this.$root.$emit("bv::show::tooltip", "copyPidTooltip");
} }
}, },
getPidSuffix(): string { async validatePid() {
if (this.pid && this.pid.lastIndexOf("/") !== -1) { this.isLoading = true;
return this.pid.split("/")[1];
}
return "";
},
async validatePid(): Promise<boolean | null> {
if (this.pid) { if (this.pid) {
return await this.pidStore.isPidValid(this.pid); const prefix = this.pid.split("/").at(0);
} else return null; const id = this.pid.split("/").at(1);
if (prefix && id) {
this.isPidValid = await this.pidStore.validatePid(prefix, id);
}
}
this.isLoading = false;
}, },
async clickSubmit() { async clickSubmit() {
if (this.pid && this.isPidValid && !this.v$.form.$invalid) { if (this.pid && this.isPidValid && !this.v$.form.$invalid) {
this.isLoading = true; this.isLoading = true;
this.form.pid = this.pid; const prefix = this.pid.split("/").at(0);
this.form.guid = this.getPidSuffix(); const id = this.pid.split("/").at(1);
const success = await this.pidStore.contactPidOwner(this.form); if (prefix && id) {
const success = await this.pidStore.contactPidOwner(
prefix,
id,
this.form
);
if (success) { if (success) {
// On Success // On Success
this.notificationStore.postNotification({ this.notificationStore.postNotification({
...@@ -230,6 +241,7 @@ export default defineComponent({ ...@@ -230,6 +241,7 @@ export default defineComponent({
body: this.$t("page.pid.toastEmailSent.body").toString(), body: this.$t("page.pid.toastEmailSent.body").toString(),
}); });
} }
}
// Clear the form // Clear the form
this.form.name = ""; this.form.name = "";
this.form.email = ""; this.form.email = "";
......
...@@ -5,7 +5,8 @@ import useNotificationStore from "@/store/notification"; ...@@ -5,7 +5,8 @@ import useNotificationStore from "@/store/notification";
import { PidApi } from "@coscine/api-client"; import { PidApi } from "@coscine/api-client";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { StatusCodes } from "http-status-codes"; import { StatusCodes } from "http-status-codes";
import type { MessageObject } from "@coscine/api-client/dist/types/Coscine.Api.Pid"; import type { PidEnquiryDto } from "@coscine/api-client/dist/types/Coscine.Api/api";
/* /*
Store variable name is "this.<id>Store" Store variable name is "this.<id>Store"
id: "pid" --> this.pidStore id: "pid" --> this.pidStore
...@@ -41,11 +42,17 @@ export const usePidStore = defineStore({ ...@@ -41,11 +42,17 @@ export const usePidStore = defineStore({
@click = "this.pidStore.<action_name>(); @click = "this.pidStore.<action_name>();
*/ */
actions: { actions: {
async isPidValid(pid: string): Promise<boolean | null> { /**
* Validates the PID (Prefix ID) using the provided PID prefix and ID.
* @param {string} prefix - The PID prefix value.
* @param {string} id - The ID value.
* @returns {Promise<boolean | null>} A promise that resolves to a boolean indicating whether the PID is valid, or null if there was an error.
*/
async validatePid(prefix: string, id: string): Promise<boolean | null> {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
const apiResponse = await PidApi.pidIsValid(pid); const apiResponse = await PidApi.validatePid(prefix, id);
return apiResponse.status === StatusCodes.OK ? true : false; return apiResponse.data.isSuccess ?? false;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
...@@ -53,13 +60,27 @@ export const usePidStore = defineStore({ ...@@ -53,13 +60,27 @@ export const usePidStore = defineStore({
} }
}, },
/**
* Contacts the PID owner using the provided PID prefix, ID, and PID enquiry data.
* @param {string} prefix - The PID prefix value.
* @param {string} id - The ID value.
* @param {PidEnquiryDto} pidEnquiryDto - The PID enquiry data.
* @returns {Promise<boolean | null>} A promise that resolves to a boolean indicating whether the contact operation was successful, or null if there was an error.
*/
async contactPidOwner( async contactPidOwner(
messageObject: MessageObject prefix: string,
id: string,
pidEnquiryDto: PidEnquiryDto
): Promise<boolean | null> { ): Promise<boolean | null> {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
try { try {
const apiResponse = await PidApi.pidSendMailToOwner(messageObject); const apiResponse = await PidApi.sendEmailToOwner(
return apiResponse.status === StatusCodes.OK ? true : false; prefix,
id,
pidEnquiryDto
);
// Note: Beware that only 204 (No Content) is considered a success in this implementation.
return apiResponse.status === StatusCodes.NO_CONTENT ? true : false;
} catch (error) { } catch (error) {
// Handle other Status Codes // Handle other Status Codes
notificationStore.postApiErrorNotification(error as AxiosError); notificationStore.postApiErrorNotification(error as AxiosError);
......
...@@ -15,8 +15,8 @@ import useProjectStore from "./store"; ...@@ -15,8 +15,8 @@ import useProjectStore from "./store";
import useResourceStore from "@/modules/resource/store"; import useResourceStore from "@/modules/resource/store";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import type { ProjectObject } from "@coscine/api-client/dist/types/Coscine.Api.Project";
import type { Route } from "vue-router"; import type { Route } from "vue-router";
import type { ProjectDto } from "@coscine/api-client/dist/types/Coscine.Api/api";
export default defineComponent({ export default defineComponent({
async beforeRouteUpdate(to, from, next) { async beforeRouteUpdate(to, from, next) {
...@@ -32,7 +32,7 @@ export default defineComponent({ ...@@ -32,7 +32,7 @@ export default defineComponent({
}, },
computed: { computed: {
project(): ProjectObject | null { project(): ProjectDto | null {
return this.projectStore.currentProject; return this.projectStore.currentProject;
}, },
moduleIsReady(): boolean { moduleIsReady(): boolean {
...@@ -70,10 +70,6 @@ export default defineComponent({ ...@@ -70,10 +70,6 @@ export default defineComponent({
if (this.projectStore.currentResources === null) { if (this.projectStore.currentResources === null) {
this.projectStore.retrieveResources(this.project); this.projectStore.retrieveResources(this.project);
} }
// Load Sub-Projects for the project if not present
if (this.projectStore.currentSubProjects === null) {
this.projectStore.retrieveSubProjects(this.project);
}
// Load Project Roles for the project if not present // Load Project Roles for the project if not present
if (this.projectStore.currentProjectRoles === null) { if (this.projectStore.currentProjectRoles === null) {
await this.projectStore.retrieveProjectRoles(this.project); await this.projectStore.retrieveProjectRoles(this.project);
...@@ -90,7 +86,10 @@ export default defineComponent({ ...@@ -90,7 +86,10 @@ export default defineComponent({
) { ) {
this.projectStore.retrieveInvitations(this.project); this.projectStore.retrieveInvitations(this.project);
} }
await this.resourceStore.retrieveEnabledResourceTypes(this.project); // Load list of Available Resource Types for the project if not present
await this.resourceStore.retrieveAvailableResourceTypesInformationForProject(
this.project
);
}, },
}, },
}); });
......
import type { OrganizationObject } from "@coscine/api-client/dist/types/Coscine.Api.Organization"; import type { OrganizationDto } from "@coscine/api-client/dist/types/Coscine.Api";
export const defaultOrganizations = [ export const defaultOrganizations: OrganizationDto[] = [
{ {
displayName: "FH Aachen", name: "FH Aachen",
url: "https://ror.org/04tqgg260", rorUri: "https://ror.org/04tqgg260",
} as OrganizationObject, },
{ {
displayName: "Karlsruhe Institute of Technology", name: "Karlsruhe Institute of Technology",
url: "https://ror.org/04t3en479", rorUri: "https://ror.org/04t3en479",
} as OrganizationObject, },
{ {
displayName: "RWTH Aachen University", name: "RWTH Aachen University",
url: "https://ror.org/04xfq0f34", rorUri: "https://ror.org/04xfq0f34",
} as OrganizationObject, },
{ {
displayName: "Technical University of Munich", name: "Technical University of Munich",
url: "https://ror.org/02kkvpp62", rorUri: "https://ror.org/02kkvpp62",
} as OrganizationObject, },
{ {
displayName: "TU Darmstadt", name: "TU Darmstadt",
url: "https://ror.org/05n911h24", rorUri: "https://ror.org/05n911h24",
} as OrganizationObject, },
{ {
displayName: "TU Dortmund University", name: "TU Dortmund University",
url: "https://ror.org/01k97gp34", rorUri: "https://ror.org/01k97gp34",
} as OrganizationObject, },
{ {
displayName: "Universitätsklinikum Aachen", name: "Universitätsklinikum Aachen",
url: "https://ror.org/02gm5zw39", rorUri: "https://ror.org/02gm5zw39",
} as OrganizationObject, },
{ {
displayName: "University of Cologne", name: "University of Cologne",
url: "https://ror.org/00rcxh774", rorUri: "https://ror.org/00rcxh774",
} as OrganizationObject, },
{ {
displayName: "University of Duisburg-Essen", name: "University of Duisburg-Essen",
url: "https://ror.org/04mz5ra38", rorUri: "https://ror.org/04mz5ra38",
} as OrganizationObject, },
{ {
displayName: "University of Münster", name: "University of Münster",
url: "https://ror.org/00pd74e08", rorUri: "https://ror.org/00pd74e08",
} as OrganizationObject, },
] as OrganizationObject[]; ];
...@@ -21,7 +21,7 @@ import type Vue from "vue"; ...@@ -21,7 +21,7 @@ import type Vue from "vue";
/* Import of relevant mockup data */ /* Import of relevant mockup data */
import { testProjectState } from "@/data/mockup/testProject"; import { testProjectState } from "@/data/mockup/testProject";
import { testUserState } from "@/data/mockup/testUser"; import { getTestUserState } from "@/data/mockup/testUser";
/* Create a local Vue instance */ /* Create a local Vue instance */
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -31,7 +31,7 @@ describe("ConfigurationMetadata.vue", () => { ...@@ -31,7 +31,7 @@ describe("ConfigurationMetadata.vue", () => {
/* Describe Pre-initialization steps */ /* Describe Pre-initialization steps */
/* Description of the test */ /* Description of the test */
test("ProjectEditing", async () => { test("should enable buttons and update project name", async () => {
/* Test Pre-initialization steps */ /* Test Pre-initialization steps */
/* Mount the Component */ /* Mount the Component */
...@@ -40,38 +40,39 @@ describe("ConfigurationMetadata.vue", () => { ...@@ -40,38 +40,39 @@ describe("ConfigurationMetadata.vue", () => {
createSpy: vitest.fn, createSpy: vitest.fn,
initialState: { initialState: {
project: testProjectState, project: testProjectState,
user: testUserState, user: getTestUserState(),
}, },
}), }),
i18n, i18n,
localVue, localVue,
}); });
/* Content of the test ... */ // Check initial state of buttons
/* Delete button */
expect(wrapper.get("#DeleteProjectBtn").attributes()["disabled"]).not.toBe( expect(wrapper.get("#DeleteProjectBtn").attributes()["disabled"]).not.toBe(
"disabled" "disabled"
); ); // Delete button - active
/* Submit button */
expect(wrapper.get("#SubmitProjectBtn").attributes()["disabled"]).toBe( expect(wrapper.get("#SubmitProjectBtn").attributes()["disabled"]).toBe(
"disabled" "disabled"
); ); // Submit button - disabled
await wrapper.vm.$nextTick();
/* find element (Project Name) */ // Find element (Project Name)
const element = wrapper.get("#ProjectName"); const element = wrapper.get("#ProjectName");
expect(element.exists()).toBe(true); expect(element.exists()).toBe(true);
/* change value of element */ // Change value of element
await element.setValue("New Test Project"); await element.setValue("New Test Project");
expect(wrapper.vm.$data.projectForm.projectName).toBe("New Test Project"); expect(wrapper.vm.$data.projectForUpdate.projectName).toBe(
"New Test Project"
);
/* Buttons should be enabled */ // Buttons should be enabled
expect(wrapper.get("#DeleteProjectBtn").attributes()["disabled"]).not.toBe( expect(wrapper.get("#DeleteProjectBtn").attributes()["disabled"]).not.toBe(
"disabled" "disabled"
); ); // Delete button - active
expect(wrapper.get("#SubmitProjectBtn").attributes()["disabled"]).not.toBe( expect(wrapper.get("#SubmitProjectBtn").attributes()["disabled"]).not.toBe(
"disabled" "disabled"
); ); // Submit button - active
}); });
}); });
...@@ -6,20 +6,20 @@ ...@@ -6,20 +6,20 @@
<div class="col-sm-8"> <div class="col-sm-8">
<b-form id="edit_form" @submit.stop.prevent="clickSave"> <b-form id="edit_form" @submit.stop.prevent="clickSave">
<FormNaming <FormNaming
v-model="projectForm" v-model="projectForUpdate"
:disabled="!isOwner" :disabled="!isOwner"
:is-loading="isLoading" :is-loading="isLoading"
@validation="formValidations.naming = $event" @validation="formValidations.naming = $event"
/><!-- TODO: Fix @validation assignment and typing --> /><!-- TODO: Fix @validation assignment and typing -->
<FormReadonlyData <FormReadonlyData
v-model="projectForm" v-model="projectForUpdate"
:disabled="!isOwner" :disabled="!isOwner"
:is-loading="isLoading" :is-loading="isLoading"
/> />
<FormMetadata <FormMetadata
v-model="projectForm" v-model="projectForUpdate"
:disabled="!isOwner" :disabled="!isOwner"
:is-loading="isLoading" :is-loading="isLoading"
:current-project="project" :current-project="project"
...@@ -43,8 +43,8 @@ ...@@ -43,8 +43,8 @@
" "
@click.prevent="clickSave" @click.prevent="clickSave"
> >
{{ $t("buttons.submit") }}</b-button {{ $t("buttons.submit") }}
> </b-button>
<!-- Delete Button --> <!-- Delete Button -->
<b-button <b-button
...@@ -54,8 +54,8 @@ ...@@ -54,8 +54,8 @@
class="float-left" class="float-left"
@click.prevent="isDeleteModalVisible = true" @click.prevent="isDeleteModalVisible = true"
> >
{{ $t("buttons.delete") }}</b-button {{ $t("buttons.delete") }}
> </b-button>
</b-form-group> </b-form-group>
</b-form> </b-form>
...@@ -87,13 +87,16 @@ import useProjectStore from "../store"; ...@@ -87,13 +87,16 @@ import useProjectStore from "../store";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import { navigateToProject } from "@/router"; import { navigateToProject } from "@/router";
import type {
ProjectObject,
DisciplineObject,
OrganizationObject,
VisibilityObject,
} from "@coscine/api-client/dist/types/Coscine.Api.Project";
import type { Validation } from "@vuelidate/core"; import type { Validation } from "@vuelidate/core";
import type {
ProjectDto,
ProjectForUpdateDto,
VisibilityDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
import {
ProjectDto2ProjectForUpdateDto,
projectMapper,
} from "@/mapping/project";
export default defineComponent({ export default defineComponent({
components: { components: {
...@@ -111,26 +114,25 @@ export default defineComponent({ ...@@ -111,26 +114,25 @@ export default defineComponent({
data() { data() {
return { return {
projectForm: { projectForUpdate: {
description: "", description: "",
displayName: "", displayName: "",
id: "",
pid: "", pid: "",
startDate: "", startDate: "",
endDate: "", endDate: "",
keywords: "", keywords: [] as string[],
projectName: "", projectName: "",
principleInvestigators: "", principleInvestigators: "",
grantId: "", grantId: "",
slug: "", slug: "",
disciplines: [] as DisciplineObject[], disciplines: [],
organizations: [] as OrganizationObject[], organizations: [],
visibility: {} as VisibilityObject, visibility: {} as VisibilityDto,
} as ProjectObject, } as ProjectForUpdateDto,
formValidations: { formValidations: {
naming: {} as Validation<ProjectObject>, naming: {} as Validation<ProjectDto>,
metadata: {} as Validation<ProjectObject>, metadata: {} as Validation<ProjectDto>,
}, },
isLoading: false, isLoading: false,
...@@ -140,10 +142,10 @@ export default defineComponent({ ...@@ -140,10 +142,10 @@ export default defineComponent({
}, },
computed: { computed: {
project(): ProjectObject | null { project(): ProjectDto | null {
return this.projectStore.currentProject; return this.projectStore.currentProject;
}, },
parentProject(): ProjectObject | null { parentProject(): ProjectDto | null {
if ( if (
this.projectStore.currentParentProjects && this.projectStore.currentParentProjects &&
this.projectStore.currentParentProjects.length > 0 this.projectStore.currentParentProjects.length > 0
...@@ -167,9 +169,9 @@ export default defineComponent({ ...@@ -167,9 +169,9 @@ export default defineComponent({
}, },
}, },
created() { async created() {
this.isLoading = true; this.isLoading = true;
this.fetchData(); await this.fetchData();
// Fill the project form // Fill the project form
this.onProjectLoaded(); this.onProjectLoaded();
this.isLoading = false; this.isLoading = false;
...@@ -189,8 +191,12 @@ export default defineComponent({ ...@@ -189,8 +191,12 @@ export default defineComponent({
onProjectLoaded() { onProjectLoaded() {
if (this.project) { if (this.project) {
this.isLoading = true; this.isLoading = true;
// Fill the form. Note that regular assignment makes this.project react on this.projectForm changes! // Fill the form. Note that regular assignment breaks reactivity!
Object.assign(this.projectForm, this.project); // Use this to only copy the properties const mapped = projectMapper.map(
ProjectDto2ProjectForUpdateDto,
this.project
);
Object.assign(this.projectForUpdate, mapped); // Use this to only copy the properties
this.isLoading = false; this.isLoading = false;
} }
}, },
...@@ -229,6 +235,7 @@ export default defineComponent({ ...@@ -229,6 +235,7 @@ export default defineComponent({
this.formValidations.metadata.$touch(); this.formValidations.metadata.$touch();
// Check if there are errors in each form and if they were changed // Check if there are errors in each form and if they were changed
if ( if (
!(this.project && this.project.id) ||
this.formValidations.naming.$invalid || this.formValidations.naming.$invalid ||
this.formValidations.metadata.$invalid || this.formValidations.metadata.$invalid ||
(!this.formValidations.naming.$anyDirty && (!this.formValidations.naming.$anyDirty &&
...@@ -237,8 +244,14 @@ export default defineComponent({ ...@@ -237,8 +244,14 @@ export default defineComponent({
console.error("The form is invalid!"); console.error("The form is invalid!");
return; return;
} }
this.isWaitingForResponse = true; this.isWaitingForResponse = true;
const success = await this.projectStore.updateProject(this.projectForm);
const success = await this.projectStore.updateProject(
this.project.id,
this.projectForUpdate
);
this.isWaitingForResponse = false; this.isWaitingForResponse = false;
if (success) { if (success) {
// On Success // On Success
...@@ -262,5 +275,3 @@ export default defineComponent({ ...@@ -262,5 +275,3 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style scoped></style>
...@@ -18,13 +18,13 @@ ...@@ -18,13 +18,13 @@
</b-form-group> </b-form-group>
<FormNaming <FormNaming
v-model="projectForm" v-model="projectForCreation"
:is-loading="isLoading" :is-loading="isLoading"
@validation="formValidations.naming = $event" @validation="formValidations.naming = $event"
/><!-- TODO: Fix @validation assignment and typing --> /><!-- TODO: Fix @validation assignment and typing -->
<FormMetadata <FormMetadata
v-model="projectForm" v-model="projectForCreation"
:is-loading="isLoading" :is-loading="isLoading"
:parent-project="project" :parent-project="project"
@validation="formValidations.metadata = $event" @validation="formValidations.metadata = $event"
...@@ -67,20 +67,19 @@ import moment from "moment"; ...@@ -67,20 +67,19 @@ import moment from "moment";
import FormNaming from "./components/FormNaming.vue"; import FormNaming from "./components/FormNaming.vue";
import FormMetadata from "./components/FormMetadata.vue"; import FormMetadata from "./components/FormMetadata.vue";
import type {
ProjectObject,
DisciplineObject,
OrganizationObject,
VisibilityObject,
} from "@coscine/api-client/dist/types/Coscine.Api.Project";
import type { Validation } from "@vuelidate/core"; import type { Validation } from "@vuelidate/core";
// import the store for current module // import the store for current module
import useProjectStore from "../store"; import useProjectStore from "../store";
import useResourceStore from "@/modules/resource/store"; import useResourceStore from "@/modules/resource/store";
import useUserStore from "@/modules/user/store";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import { navigateToProject } from "@/router"; import { navigateToProject } from "@/router";
import type {
ProjectDto,
ProjectForCreationDto,
VisibilityForProjectManipulationDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
export default defineComponent({ export default defineComponent({
components: { components: {
...@@ -90,26 +89,27 @@ export default defineComponent({ ...@@ -90,26 +89,27 @@ export default defineComponent({
setup() { setup() {
const projectStore = useProjectStore(); const projectStore = useProjectStore();
const resourceStore = useResourceStore(); const resourceStore = useResourceStore();
const userStore = useUserStore();
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
return { projectStore, resourceStore, notificationStore }; return { projectStore, resourceStore, userStore, notificationStore };
}, },
data() { data() {
return { return {
projectForm: { projectForCreation: {
description: "", description: "",
displayName: "", displayName: "",
startDate: moment(new Date()).format("YYYY-MM-DD"), startDate: moment(new Date()).format("YYYY-MM-DD"),
endDate: moment(new Date()).format("YYYY-MM-DD"), endDate: moment(new Date()).format("YYYY-MM-DD"),
keywords: "", keywords: [] as string[],
projectName: "", projectName: "",
principleInvestigators: "", principleInvestigators: "",
grantId: "", grantId: "",
disciplines: [] as DisciplineObject[], disciplines: [],
organizations: [] as OrganizationObject[], organizations: [],
visibility: {} as VisibilityObject, visibility: {} as VisibilityForProjectManipulationDto,
} as ProjectObject, } as ProjectForCreationDto,
formValidations: { formValidations: {
naming: {} as Validation, naming: {} as Validation,
...@@ -117,13 +117,21 @@ export default defineComponent({ ...@@ -117,13 +117,21 @@ export default defineComponent({
}, },
isLoading: false, isLoading: false,
isRWTHMember: false,
isWaitingForResponse: false, isWaitingForResponse: false,
}; };
}, },
computed: { computed: {
project(): ProjectObject | null { isRWTHMember(): boolean {
const memberships = this.userStore.user?.organizations;
if (memberships) {
return memberships.some(
(membership) => membership.rorUri === "https://ror.org/04xfq0f34"
);
}
return false;
},
project(): ProjectDto | null {
return this.projectStore.currentProject; return this.projectStore.currentProject;
}, },
}, },
...@@ -144,10 +152,10 @@ export default defineComponent({ ...@@ -144,10 +152,10 @@ export default defineComponent({
if (this.projectStore.disciplines === null) { if (this.projectStore.disciplines === null) {
await this.projectStore.retrieveDisciplines(); await this.projectStore.retrieveDisciplines();
} }
// Load User Memberships if not present
this.isRWTHMember = await this.projectStore.getIsMemberOfOrganization( if (this.userStore.user === null) {
"https://ror.org/04xfq0f34" // RWTH Aachen University RoR await this.userStore.retrieveUser();
); }
}, },
async clickSave() { async clickSave() {
...@@ -167,7 +175,7 @@ export default defineComponent({ ...@@ -167,7 +175,7 @@ export default defineComponent({
// Trigger API call to create a new project // Trigger API call to create a new project
this.isWaitingForResponse = true; this.isWaitingForResponse = true;
const createdProject = await this.projectStore.storeProject( const createdProject = await this.projectStore.storeProject(
this.projectForm this.projectForCreation
); );
if (createdProject) { if (createdProject) {
// On Success // On Success
......
...@@ -23,9 +23,9 @@ ...@@ -23,9 +23,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { ProjectDto } from "@coscine/api-client/dist/types/Coscine.Api/api";
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import type { RawLocation } from "vue-router"; import type { RawLocation } from "vue-router";
import type { ProjectObject } from "@coscine/api-client/dist/types/Coscine.Api.Project";
// import the store for current module // import the store for current module
import useProjectStore from "../store"; import useProjectStore from "../store";
...@@ -38,7 +38,7 @@ export default defineComponent({ ...@@ -38,7 +38,7 @@ export default defineComponent({
}, },
computed: { computed: {
projects(): ProjectObject[] | null { projects(): ProjectDto[] | null {
return this.projectStore.topLevelProjects; return this.projectStore.topLevelProjects;
}, },
}, },
...@@ -53,7 +53,7 @@ export default defineComponent({ ...@@ -53,7 +53,7 @@ export default defineComponent({
const route = { name: "create-project" } as RawLocation; const route = { name: "create-project" } as RawLocation;
return route; return route;
}, },
toProject(project: ProjectObject): RawLocation { toProject(project: ProjectDto): RawLocation {
const route = { const route = {
name: "project-page", name: "project-page",
params: { slug: project.slug }, params: { slug: project.slug },
...@@ -65,7 +65,7 @@ export default defineComponent({ ...@@ -65,7 +65,7 @@ export default defineComponent({
this.projectStore.currentSlug = null; this.projectStore.currentSlug = null;
this.$router.push(to); this.$router.push(to);
}, },
openProject(to: RawLocation, project: ProjectObject) { openProject(to: RawLocation, project: ProjectDto) {
if (project.slug) { if (project.slug) {
this.projectStore.currentSlug = project.slug; this.projectStore.currentSlug = project.slug;
this.projectStore.setProjectAsVisited(project); this.projectStore.setProjectAsVisited(project);
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
:member-role="memberRole" :member-role="memberRole"
:project="project" :project="project"
:roles="roles" :roles="roles"
:project-members="projectMembers"
:is-owner="isOwner" :is-owner="isOwner"
@addUser="addUser" @addUser="addUser"
@prepareInvitation="prepareInvitation" @prepareInvitation="prepareInvitation"
...@@ -130,13 +131,15 @@ import useUserStore from "@/modules/user/store"; ...@@ -130,13 +131,15 @@ import useUserStore from "@/modules/user/store";
import useNotificationStore from "@/store/notification"; import useNotificationStore from "@/store/notification";
import type { import type {
InvitationReturnObject, ProjectDto,
ProjectObject, ProjectInvitationDto,
ProjectRoleObject, ProjectInvitationForProjectManipulationDto,
RoleObject, ProjectRoleDto,
SendInvitationObject, ProjectRoleForProjectCreationDto,
} from "@coscine/api-client/dist/types/Coscine.Api.Project"; ProjectRoleForProjectManipulationDto,
import type { ExtendedSendInvitationObject } from "../types"; RoleDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
import type { ExtendedProjectInvitationDto } from "../types";
export default defineComponent({ export default defineComponent({
components: { components: {
...@@ -161,14 +164,14 @@ export default defineComponent({ ...@@ -161,14 +164,14 @@ export default defineComponent({
isDeleteInvitedUserModalVisible: false, isDeleteInvitedUserModalVisible: false,
isDeleteUserModalVisible: false, isDeleteUserModalVisible: false,
// For external users // For external users
selectedInvitation: {} as InvitationReturnObject, selectedInvitation: {} as ProjectInvitationDto,
isRevokeInvitationModalVisible: false, isRevokeInvitationModalVisible: false,
// Other // Other
isBusy: false, isBusy: false,
filteredRows: 0, filteredRows: 0,
filter: "", filter: "",
candidateForDeletion: {} as ProjectRoleObject, candidateForDeletion: {} as ProjectRoleDto,
candidateForInvitation: {} as ExtendedSendInvitationObject, candidateForInvitation: {} as ExtendedProjectInvitationDto,
roleStrings: { roleStrings: {
member: "Member", member: "Member",
owner: "Owner", owner: "Owner",
...@@ -177,10 +180,10 @@ export default defineComponent({ ...@@ -177,10 +180,10 @@ export default defineComponent({
}, },
computed: { computed: {
invitations(): InvitationReturnObject[] | null { invitations(): ProjectInvitationDto[] | null {
return this.projectStore.currentInvitations; return this.projectStore.currentInvitations;
}, },
memberRole(): RoleObject | null | undefined { memberRole(): RoleDto | null | undefined {
if (this.roles) { if (this.roles) {
return this.roles.find( return this.roles.find(
(role) => role.displayName === this.roleStrings.member (role) => role.displayName === this.roleStrings.member
...@@ -188,7 +191,7 @@ export default defineComponent({ ...@@ -188,7 +191,7 @@ export default defineComponent({
} }
return null; return null;
}, },
ownerRole(): RoleObject | null | undefined { ownerRole(): RoleDto | null | undefined {
if (this.roles) { if (this.roles) {
return this.roles.find( return this.roles.find(
(role) => role.displayName === this.roleStrings.owner (role) => role.displayName === this.roleStrings.owner
...@@ -204,16 +207,19 @@ export default defineComponent({ ...@@ -204,16 +207,19 @@ export default defineComponent({
} }
return 0; return 0;
}, },
project(): ProjectObject | null { project(): ProjectDto | null {
return this.projectStore.currentProject; return this.projectStore.currentProject;
}, },
projectName(): string | null | undefined { projectName(): string | null | undefined {
return this.project?.displayName; return this.project?.displayName;
}, },
projectRoles(): ProjectRoleObject[] | null { projectRoles(): ProjectRoleDto[] | null {
return this.projectStore.currentProjectRoles; return this.projectStore.currentProjectRoles;
}, },
roles(): RoleObject[] | null { projectMembers() {
return this.projectRoles?.map((pr) => pr.user);
},
roles(): RoleDto[] | null {
return this.projectStore.roles; return this.projectStore.roles;
}, },
memberHeaders() { memberHeaders() {
...@@ -222,12 +228,12 @@ export default defineComponent({ ...@@ -222,12 +228,12 @@ export default defineComponent({
return [ return [
{ {
label: this.$t("page.members.firstName"), label: this.$t("page.members.firstName"),
key: "user.givenname", key: "user.firstName",
sortable: true, sortable: true,
}, },
{ {
label: this.$t("page.members.lastName"), label: this.$t("page.members.lastName"),
key: "user.surname", key: "user.lastName",
sortable: true, sortable: true,
}, },
{ {
...@@ -281,12 +287,13 @@ export default defineComponent({ ...@@ -281,12 +287,13 @@ export default defineComponent({
methods: { methods: {
// For external users // For external users
async storeNewInvitation( async storeNewInvitation(
value: SendInvitationObject, projectId: string,
value: ProjectInvitationForProjectManipulationDto,
toastText: string, toastText: string,
errorText: string errorText: string
) { ) {
try { try {
await this.projectStore.storeInvitation(value); await this.projectStore.storeInvitation(projectId, value);
await this.projectStore.retrieveInvitations(this.project); await this.projectStore.retrieveInvitations(this.project);
this.notificationStore.postNotification({ this.notificationStore.postNotification({
title: this.$t("page.members.userManagement").toString(), title: this.$t("page.members.userManagement").toString(),
...@@ -301,11 +308,10 @@ export default defineComponent({ ...@@ -301,11 +308,10 @@ export default defineComponent({
} }
}, },
resendInvitation(selectedInvitation: InvitationReturnObject) { resendInvitation(selectedInvitation: ProjectInvitationDto) {
const invitation: SendInvitationObject = { const invitation: ProjectInvitationForProjectManipulationDto = {
projectId: selectedInvitation.projectId, roleId: selectedInvitation.roleId ?? "",
role: selectedInvitation.roleId, email: selectedInvitation.userMail ?? "",
email: selectedInvitation.userMail,
}; };
const errorText = this.$t("page.members.invitedUserError", { const errorText = this.$t("page.members.invitedUserError", {
email: selectedInvitation.userMail, email: selectedInvitation.userMail,
...@@ -317,12 +323,19 @@ export default defineComponent({ ...@@ -317,12 +323,19 @@ export default defineComponent({
: "", : "",
projectName: this.projectName, projectName: this.projectName,
}).toString(); }).toString();
this.storeNewInvitation(invitation, text, errorText); this.storeNewInvitation(
this.project?.id ?? "",
invitation,
text,
errorText
);
}, },
revokeInvitation(selectedInvitation: InvitationReturnObject) { revokeInvitation(selectedInvitation: ProjectInvitationDto) {
this.selectedInvitation.id = selectedInvitation.id; this.selectedInvitation.id = selectedInvitation.id;
this.selectedInvitation.userMail = selectedInvitation.userMail; this.selectedInvitation.userMail = selectedInvitation.userMail;
this.selectedInvitation.projectId = selectedInvitation.projectId;
this.isDeleteInvitedUserModalVisible = true; this.isDeleteInvitedUserModalVisible = true;
}, },
...@@ -333,7 +346,7 @@ export default defineComponent({ ...@@ -333,7 +346,7 @@ export default defineComponent({
project: this.projectName, project: this.projectName,
}).toString(); }).toString();
const worked = await this.projectStore.deleteInvitation( const worked = await this.projectStore.deleteInvitation(
this.selectedInvitation.id this.selectedInvitation
); );
if (worked) { if (worked) {
await this.projectStore.retrieveInvitations(this.project); await this.projectStore.retrieveInvitations(this.project);
...@@ -366,10 +379,10 @@ export default defineComponent({ ...@@ -366,10 +379,10 @@ export default defineComponent({
this.isBusy = false; this.isBusy = false;
}, },
async setRole(projectRole: ProjectRoleObject) { async setRole(projectRole: ProjectRoleDto) {
if (this.project) { if (this.project && this.project.id && projectRole.id) {
projectRole.projectId = this.project.id; projectRole.projectId = this.project.id;
}
if (projectRole.role) { if (projectRole.role) {
projectRole.role.displayName = projectRole.role.displayName =
projectRole.role.displayName === this.roleStrings.member projectRole.role.displayName === this.roleStrings.member
...@@ -389,12 +402,19 @@ export default defineComponent({ ...@@ -389,12 +402,19 @@ export default defineComponent({
: "", : "",
}).toString(); }).toString();
await this.projectStore.storeProjectRole(projectRole); await this.projectStore.updateMembershipOfProject(
this.project.id,
projectRole.id,
{
roleId: projectRole.role?.id,
} as ProjectRoleForProjectManipulationDto
);
await this.getProjectRoles(); await this.getProjectRoles();
this.notificationStore.postNotification({ this.notificationStore.postNotification({
title: this.$t("page.members.userManagement").toString(), title: this.$t("page.members.userManagement").toString(),
body: text, body: text,
}); });
}
}, },
getRoleNameFromId(roleId: string): string | null | undefined { getRoleNameFromId(roleId: string): string | null | undefined {
...@@ -409,10 +429,9 @@ export default defineComponent({ ...@@ -409,10 +429,9 @@ export default defineComponent({
} }
}, },
async addUser(projectRole: ProjectRoleObject, callback?: () => void) { async addUser(projectRole: ProjectRoleDto, callback?: () => void) {
if (this.project) { if (this.project && this.project.id) {
projectRole.projectId = this.project.id; projectRole.projectId = this.project.id;
}
const text = this.$t("page.members.addedUser", { const text = this.$t("page.members.addedUser", {
project: this.projectName, project: this.projectName,
...@@ -426,7 +445,10 @@ export default defineComponent({ ...@@ -426,7 +445,10 @@ export default defineComponent({
: "", : "",
}).toString(); }).toString();
await this.projectStore.storeProjectRole(projectRole); await this.projectStore.addMembershipToProject(this.project.id, {
roleId: projectRole.role?.id,
userId: projectRole.user?.userId,
} as ProjectRoleForProjectCreationDto);
await this.getProjectRoles(); await this.getProjectRoles();
this.notificationStore.postNotification({ this.notificationStore.postNotification({
title: this.$t("page.members.userManagement").toString(), title: this.$t("page.members.userManagement").toString(),
...@@ -435,13 +457,14 @@ export default defineComponent({ ...@@ -435,13 +457,14 @@ export default defineComponent({
if (callback) { if (callback) {
callback(); callback();
} }
}
}, },
prepareInvitation(projectRole: ProjectRoleObject) { prepareInvitation(projectRole: ProjectRoleDto) {
this.candidateForInvitation = { this.candidateForInvitation = {
projectId: projectRole.projectId, projectId: this.project?.id ?? "",
role: projectRole.role?.id, roleId: projectRole.role?.id ?? "",
email: projectRole.user?.emailAddress, email: projectRole.user?.emailAddress ?? "",
expired: false, expired: false,
invited: false, invited: false,
}; };
...@@ -472,7 +495,7 @@ export default defineComponent({ ...@@ -472,7 +495,7 @@ export default defineComponent({
} }
}, },
prepareDeletion(projectRole: ProjectRoleObject) { prepareDeletion(projectRole: ProjectRoleDto) {
this.candidateForDeletion = projectRole; this.candidateForDeletion = projectRole;
this.isDeleteUserModalVisible = true; this.isDeleteUserModalVisible = true;
}, },
...@@ -495,7 +518,7 @@ export default defineComponent({ ...@@ -495,7 +518,7 @@ export default defineComponent({
this.isDeleteUserModalVisible = false; this.isDeleteUserModalVisible = false;
}, },
onFiltered(filteredItems: ProjectRoleObject[]) { onFiltered(filteredItems: ProjectRoleDto[]) {
this.filteredRows = filteredItems.length; this.filteredRows = filteredItems.length;
}, },
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
<!-- Content Cards --> <!-- Content Cards -->
<CoscineCard <CoscineCard
v-for="(resource, index) in resourceList" v-for="(resource, index) in resources"
:key="index" :key="index"
:title="resource.displayName" :title="resource.displayName"
type="resource" type="resource"
...@@ -107,16 +107,16 @@ ...@@ -107,16 +107,16 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import MembersList from "./components/MembersList.vue"; import MembersList from "./components/MembersList.vue";
import type { RawLocation } from "vue-router"; import type { RawLocation } from "vue-router";
import type {
ProjectObject,
ResourceObject,
} from "@coscine/api-client/dist/types/Coscine.Api.Project";
import type { UserObject } from "@coscine/api-client/dist/types/Coscine.Api.User";
// import the store for current module // import the store for current module
import useProjectStore from "../store"; import useProjectStore from "../store";
import useResourceStore from "@/modules/resource/store"; import useResourceStore from "@/modules/resource/store";
import useUserStore from "@/modules/user/store"; import useUserStore from "@/modules/user/store";
import type {
ProjectDto,
ResourceDto,
UserDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
export default defineComponent({ export default defineComponent({
components: { components: {
...@@ -131,29 +131,17 @@ export default defineComponent({ ...@@ -131,29 +131,17 @@ export default defineComponent({
}, },
computed: { computed: {
project(): ProjectObject | null { project(): ProjectDto | null {
return this.projectStore.currentProject; return this.projectStore.currentProject;
}, },
subProjects(): ProjectObject[] | null { subProjects(): ProjectDto[] | null {
return this.projectStore.currentSubProjects; return this.projectStore.currentSubProjects;
}, },
resourceList(): ResourceObject[] { resources(): ResourceDto[] | null {
return this.resources ? this.resources : [];
},
resources(): ResourceObject[] | null {
return this.projectStore.currentResources; return this.projectStore.currentResources;
}, },
isEmailValid(): boolean { isEmailValid(): boolean {
if ( return this.user?.isEmailConfirmed ?? false;
this.user &&
this.user.emailAddress &&
this.user.emailAddress.trim() !== ""
) {
// Assume API contains only valid email addresses
return true;
} else {
return false;
}
}, },
isGuest(): boolean | undefined { isGuest(): boolean | undefined {
return this.projectStore.currentUserRoleIsGuest; return this.projectStore.currentUserRoleIsGuest;
...@@ -161,7 +149,7 @@ export default defineComponent({ ...@@ -161,7 +149,7 @@ export default defineComponent({
isLoadingRoles(): boolean { isLoadingRoles(): boolean {
return this.projectStore.currentUserRole ? false : true; return this.projectStore.currentUserRole ? false : true;
}, },
user(): UserObject | null { user(): UserDto | null | undefined {
return this.userStore.user; return this.userStore.user;
}, },
}, },
...@@ -175,21 +163,21 @@ export default defineComponent({ ...@@ -175,21 +163,21 @@ export default defineComponent({
const route = { name: "create-resource" } as RawLocation; const route = { name: "create-resource" } as RawLocation;
return route; return route;
}, },
toSubProject(project: ProjectObject): RawLocation { toSubProject(project: ProjectDto): RawLocation {
const route = { const route = {
name: "project-page", name: "project-page",
params: { slug: project.slug }, params: { slug: project.slug },
} as RawLocation; } as RawLocation;
return route; return route;
}, },
toResource(resource: ResourceObject): RawLocation { toResource(resource: ResourceDto): RawLocation {
const route = { const route = {
name: "resource-page", name: "resource-page",
params: { guid: resource.id }, params: { guid: resource.id },
} as RawLocation; } as RawLocation;
return route; return route;
}, },
toResourceSettings(resource: ResourceObject): RawLocation | null { toResourceSettings(resource: ResourceDto): RawLocation | null {
const route = { const route = {
name: "resource-settings", name: "resource-settings",
params: { guid: resource.id }, params: { guid: resource.id },
...@@ -208,7 +196,7 @@ export default defineComponent({ ...@@ -208,7 +196,7 @@ export default defineComponent({
this.$router.push(to); this.$router.push(to);
} }
}, },
openProject(to: RawLocation, project: ProjectObject) { openProject(to: RawLocation, project: ProjectDto) {
if (project.slug) { if (project.slug) {
// Add project here, to save an unnecessary API request // Add project here, to save an unnecessary API request
this.projectStore.setProjectAsVisited(project); this.projectStore.setProjectAsVisited(project);
...@@ -219,7 +207,7 @@ export default defineComponent({ ...@@ -219,7 +207,7 @@ export default defineComponent({
); );
} }
}, },
openResource(to: RawLocation, resource: ResourceObject) { openResource(to: RawLocation, resource: ResourceDto) {
if (resource.id) { if (resource.id) {
// Add resource here, to save an unnecessary API request // Add resource here, to save an unnecessary API request
this.resourceStore.setResourceAsVisited(resource); this.resourceStore.setResourceAsVisited(resource);
...@@ -230,7 +218,7 @@ export default defineComponent({ ...@@ -230,7 +218,7 @@ export default defineComponent({
); );
} }
}, },
openResourceSettings(to: RawLocation, resource: ResourceObject) { openResourceSettings(to: RawLocation, resource: ResourceDto) {
if (resource.id) { if (resource.id) {
// Add resource here, to save an unnecessary API request // Add resource here, to save an unnecessary API request
this.resourceStore.setResourceAsVisited(resource); this.resourceStore.setResourceAsVisited(resource);
......
This diff is collapsed.
...@@ -35,14 +35,16 @@ ...@@ -35,14 +35,16 @@
> >
<b-form-input <b-form-input
id="PrincipleInvestigators" id="PrincipleInvestigators"
v-model="v$.projectForm.principleInvestigators.$model" v-model="v$.projectForManipulation.principleInvestigators.$model"
:state=" :state="
v$.projectForm.principleInvestigators.$dirty v$.projectForManipulation.principleInvestigators.$dirty
? !v$.projectForm.principleInvestigators.$error ? !v$.projectForManipulation.principleInvestigators.$error
: null : null
" "
:placeholder="$t('form.project.projectPrincipleInvestigators')" :placeholder="$t('form.project.projectPrincipleInvestigators')"
:maxlength="v$.projectForm.principleInvestigators.maxLength.$params.max" :maxlength="
v$.projectForManipulation.principleInvestigators.maxLength.$params.max
"
required required
:disabled="disabled" :disabled="disabled"
/> />
...@@ -50,7 +52,8 @@ ...@@ -50,7 +52,8 @@
{{ {{
$t("form.project.projectPrincipleInvestigatorsHelp", { $t("form.project.projectPrincipleInvestigatorsHelp", {
maxLength: maxLength:
v$.projectForm.principleInvestigators.maxLength.$params.max, v$.projectForManipulation.principleInvestigators.maxLength.$params
.max,
}) })
}} }}
</div> </div>
...@@ -66,12 +69,12 @@ ...@@ -66,12 +69,12 @@
> >
<b-form-datepicker <b-form-datepicker
id="StartDate" id="StartDate"
v-model="v$.projectForm.startDate.$model" v-model="v$.projectForManipulation.startDate.$model"
:locale="$i18n.locale" :locale="$i18n.locale"
:disabled="disabled" :disabled="disabled"
:state=" :state="
v$.projectForm.startDate.$dirty v$.projectForManipulation.startDate.$dirty
? !v$.projectForm.startDate.$error ? !v$.projectForManipulation.startDate.$error
: null : null
" "
required required
...@@ -91,12 +94,14 @@ ...@@ -91,12 +94,14 @@
> >
<b-form-datepicker <b-form-datepicker
id="EndDate" id="EndDate"
v-model="v$.projectForm.endDate.$model" v-model="v$.projectForManipulation.endDate.$model"
:locale="$i18n.locale" :locale="$i18n.locale"
:min="v$.projectForm.startDate.$model" :min="v$.projectForManipulation.startDate.$model"
:disabled="disabled" :disabled="disabled"
:state=" :state="
v$.projectForm.endDate.$dirty ? !v$.projectForm.endDate.$error : null v$.projectForManipulation.endDate.$dirty
? !v$.projectForManipulation.endDate.$error
: null
" "
required required
calendar-width="100%" calendar-width="100%"
...@@ -115,7 +120,7 @@ ...@@ -115,7 +120,7 @@
> >
<multiselect <multiselect
id="Discipline" id="Discipline"
v-model="v$.projectForm.disciplines.$model" v-model="v$.projectForManipulation.disciplines.$model"
:disabled="disabled" :disabled="disabled"
:options="disciplines" :options="disciplines"
:multiple="true" :multiple="true"
...@@ -145,7 +150,7 @@ ...@@ -145,7 +150,7 @@
:label="$t('form.project.projectOrganizationLabel')" :label="$t('form.project.projectOrganizationLabel')"
:is-loading="isLoading" :is-loading="isLoading"
type="input" type="input"
:info="true" info
> >
<!-- Hint --> <!-- Hint -->
<template #hint> <template #hint>
...@@ -159,34 +164,25 @@ ...@@ -159,34 +164,25 @@
$t('form.project.projectOrganizationLabelPopoverUrl').toString() $t('form.project.projectOrganizationLabelPopoverUrl').toString()
" "
target="_blank" target="_blank"
>{{ $t("default.help") }} >
{{ $t("default.help") }}
</b-link> </b-link>
</template> </template>
<multiselect <multiselect
id="Organization" id="Organization"
v-model="v$.projectForm.organizations.$model" v-model="v$.projectForManipulation.organizations.$model"
:disabled="disabled" :disabled="disabled"
:options="organizations" :options="organizations"
:multiple="true" :multiple="true"
:loading="isLoadingOrganizations" :loading="isLoadingOrganizations"
:hide-selected="true" :hide-selected="true"
label="displayName" label="name"
track-by="rorUri"
:show-labels="false" :show-labels="false"
track-by="url"
:placeholder="$t('form.project.projectOrganization')" :placeholder="$t('form.project.projectOrganization')"
@search-change="retrieveOrganizations" @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> <template #noOptions>
{{ $t("form.project.projectOrganizationNoOptions") }} {{ $t("form.project.projectOrganizationNoOptions") }}
</template> </template>
...@@ -201,33 +197,34 @@ ...@@ -201,33 +197,34 @@
label-for="Keywords" label-for="Keywords"
:label="$t('form.project.projectKeywordsLabel')" :label="$t('form.project.projectKeywordsLabel')"
:is-loading="isLoading" :is-loading="isLoading"
type="input"
> >
<multiselect <multiselect
id="Keywords" id="Keywords"
v-model="selectedKeyword" v-model="v$.projectForManipulation.keywords.$model"
:disabled="disabled" :options="projectForManipulation.keywords"
:options="selectedKeyword"
:placeholder="$t('form.project.projectKeywordsPlaceholder')" :placeholder="$t('form.project.projectKeywordsPlaceholder')"
:multiple="true" :multiple="true"
:taggable="true" :taggable="true"
:max="limitKeywords(selectedKeyword)" :max="limitKeywords(projectForManipulation.keywords)"
:tag-placeholder="$t('form.project.tagPlaceholder')" :tag-placeholder="$t('form.project.tagPlaceholder')"
:disabled="disabled"
:show-labels="false" :show-labels="false"
@tag="addTag" @tag="addTag"
@remove="v$.projectForm.keywords.$touch()" @remove="v$.projectForManipulation.keywords.$touch()"
> >
<template #maxElements> <template #maxElements>
{{ {{
$t("form.project.projectKeywordsHelp", { $t("form.project.projectKeywordsHelp", {
maxLength: v$.projectForm.keywords.maxLength.$params.max, maxLength:
v$.projectForManipulation.keywords.maxLength.$params.max,
}) })
}} }}
</template> </template>
<template #noOptions> <template #noOptions>
{{ {{
$t("form.project.projectKeywordsEmpty", { $t("form.project.projectKeywordsEmpty", {
maxLength: v$.projectForm.keywords.maxLength.$params.max, maxLength:
v$.projectForManipulation.keywords.maxLength.$params.max,
}) })
}} }}
</template> </template>
...@@ -241,7 +238,7 @@ ...@@ -241,7 +238,7 @@
:label="$t('form.project.projectMetadataVisibilityLabel')" :label="$t('form.project.projectMetadataVisibilityLabel')"
:is-loading="isLoading" :is-loading="isLoading"
type="input" type="input"
:info="true" info
> >
<template #popover> <template #popover>
{{ $t("form.project.projectMetadataVisibilityLabelPopover") }} {{ $t("form.project.projectMetadataVisibilityLabelPopover") }}
...@@ -252,7 +249,8 @@ ...@@ -252,7 +249,8 @@
).toString() ).toString()
" "
target="_blank" target="_blank"
>{{ $t("default.help") }} >
{{ $t("default.help") }}
</b-link> </b-link>
</template> </template>
...@@ -277,19 +275,21 @@ ...@@ -277,19 +275,21 @@
> >
<b-form-input <b-form-input
id="GrantId" id="GrantId"
v-model="v$.projectForm.grantId.$model" v-model="v$.projectForManipulation.grantId.$model"
:state=" :state="
v$.projectForm.grantId.$dirty ? !v$.projectForm.grantId.$error : null v$.projectForManipulation.grantId.$dirty
? !v$.projectForManipulation.grantId.$error
: null
" "
:placeholder="$t('form.project.projectGrantId')" :placeholder="$t('form.project.projectGrantId')"
:maxlength="v$.projectForm.grantId.maxLength.$params.max" :maxlength="v$.projectForManipulation.grantId.maxLength.$params.max"
required required
:disabled="disabled" :disabled="disabled"
/> />
<div class="invalid-tooltip"> <div class="invalid-tooltip">
{{ {{
$t("form.project.projectGrantIdHelp", { $t("form.project.projectGrantIdHelp", {
maxLength: v$.projectForm.grantId.maxLength.$params.max, maxLength: v$.projectForManipulation.grantId.maxLength.$params.max,
}) })
}} }}
</div> </div>
...@@ -300,34 +300,42 @@ ...@@ -300,34 +300,42 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, reactive, ref } from "vue"; import { defineComponent, PropType, reactive, ref } from "vue";
import { required, maxLength } from "@vuelidate/validators"; import { required, maxLength } from "@vuelidate/validators";
import { type NestedValidations, useVuelidate } from "@vuelidate/core"; import { useVuelidate, type BaseValidation } from "@vuelidate/core";
import {
DisciplineDto2DisciplineForProjectManipulationDto,
projectMapper,
OrganizationDto2OrganizationForProjectManipulationDto,
VisibilityDto2VisibilityForProjectManipulationDto,
} from "@/mapping/project";
import moment from "moment"; import moment from "moment";
import "@/plugins/deprecated/vue-multiselect"; import "@/plugins/deprecated/vue-multiselect";
import type {
DisciplineObject,
ProjectObject,
VisibilityObject,
} from "@coscine/api-client/dist/types/Coscine.Api.Project";
import type { OrganizationObject } from "@coscine/api-client/dist/types/Coscine.Api.Organization";
// import the store for current module // import the store for current module
import useProjectStore from "../../store"; import useProjectStore from "../../store";
import type {
DisciplineDto,
OrganizationDto,
ProjectDto,
ProjectForCreationDto,
ProjectForUpdateDto,
VisibilityDto,
VisibilityForProjectManipulationDto,
} from "@coscine/api-client/dist/types/Coscine.Api/api";
export default defineComponent({ export default defineComponent({
props: { props: {
value: { value: {
type: Object as PropType<ProjectObject>, type: Object as PropType<ProjectForCreationDto | ProjectForUpdateDto>,
required: true, required: true,
}, },
currentProject: { currentProject: {
default: null, default: null,
type: [Object, null] as PropType<ProjectObject | null>, type: [Object, null] as PropType<ProjectDto | null>,
required: false, required: false,
}, },
parentProject: { parentProject: {
default: null, default: null,
type: [Object, null] as PropType<ProjectObject | null>, type: [Object, null] as PropType<ProjectDto | null>,
required: false, required: false,
}, },
disabled: { disabled: {
...@@ -340,7 +348,9 @@ export default defineComponent({ ...@@ -340,7 +348,9 @@ export default defineComponent({
}, },
}, },
emits: { emits: {
validation: (_: NestedValidations<ProjectObject>) => true, validation: (
_: BaseValidation<ProjectForCreationDto | ProjectForUpdateDto>
) => true,
}, },
setup(props) { setup(props) {
...@@ -351,10 +361,10 @@ export default defineComponent({ ...@@ -351,10 +361,10 @@ export default defineComponent({
will enable proper typings in the code will enable proper typings in the code
*/ */
const state = reactive({ const state = reactive({
projectForm: ref(props.value), projectForManipulation: ref(props.value),
}); });
const rules = { const rules = {
projectForm: { projectForManipulation: {
principleInvestigators: { required, maxLength: maxLength(500) }, principleInvestigators: { required, maxLength: maxLength(500) },
startDate: { required }, startDate: { required },
endDate: { required }, endDate: { required },
...@@ -372,25 +382,24 @@ export default defineComponent({ ...@@ -372,25 +382,24 @@ export default defineComponent({
data() { data() {
return { return {
projectForm: this.value, projectForManipulation: this.value,
isLoadingOrganizations: false, isLoadingOrganizations: false,
selectedKeyword: [] as Array<string>,
selectedVisibility: "" as string | undefined, selectedVisibility: "" as string | undefined,
queryTimer: 0, queryTimer: 0,
}; };
}, },
computed: { computed: {
organizations(): OrganizationObject[] { organizations(): OrganizationDto[] {
return ( return (
this.projectStore.organizations ?? this.projectStore.organizations ??
this.projectStore.defaultOrganizations this.projectStore.defaultOrganizations
); );
}, },
visibilities(): VisibilityObject[] | null { visibilities(): VisibilityDto[] | null {
return this.projectStore.visibilities; return this.projectStore.visibilities;
}, },
disciplines(): DisciplineObject[] { disciplines(): DisciplineDto[] {
return this.projectStore.disciplines ?? []; return this.projectStore.disciplines ?? [];
}, },
disciplineLabel(): string { disciplineLabel(): string {
...@@ -413,32 +422,19 @@ export default defineComponent({ ...@@ -413,32 +422,19 @@ export default defineComponent({
this.setDefaultVisibility(); this.setDefaultVisibility();
} }
}, },
selectedKeyword() { "projectForManipulation.startDate"() {
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 // Adjust the endDate to match startDate, if startDate is past endDate
const start = moment(this.projectForm.startDate); const start = moment(this.projectForManipulation.startDate);
const end = moment(this.projectForm.endDate); const end = moment(this.projectForManipulation.endDate);
const difference = moment.duration(start.diff(end)).asDays(); const difference = moment.duration(start.diff(end)).asDays();
if (difference > 0) { if (difference > 0) {
this.projectForm.endDate = this.projectForm.startDate; this.projectForManipulation.endDate =
this.projectForManipulation.startDate;
} }
}, },
"projectForm.organizations"() { projectForManipulation: {
this.getLabels();
},
projectForm: {
handler() { handler() {
this.$emit("validation", this.v$.projectForm); this.$emit("validation", this.v$.projectForManipulation);
}, },
deep: true, deep: true,
}, },
...@@ -451,35 +447,44 @@ export default defineComponent({ ...@@ -451,35 +447,44 @@ export default defineComponent({
methods: { methods: {
onProjectLoaded() { onProjectLoaded() {
if (this.currentProject) { if (this.currentProject?.visibility?.id) {
this.loadKeywords(this.currentProject);
if (this.currentProject.visibility) {
this.selectedVisibility = this.currentProject.visibility.id; this.selectedVisibility = this.currentProject.visibility.id;
} this.setVisibility(this.selectedVisibility);
this.getLabels();
} else { } else {
this.setDefaultVisibility(); this.setDefaultVisibility();
} }
// Changes have been made to the form, reset its state
this.v$.projectForManipulation.$reset();
}, },
onParentProjectLoaded() { onParentProjectLoaded() {
if (this.parentProject) { if (this.parentProject) {
this.projectForm.parentId = this.parentProject.id; (this.projectForManipulation as ProjectForCreationDto).parentId =
this.parentProject.id;
} }
}, },
limitKeywords(values: string[]): number | null { /**
* Limits the number of keywords based on validation
* @param {string[]} values - array of keyword strings
* @return {number | null} - returns the current length of values if the keyword validation fails, else null
*/
limitKeywords(values: string[] | null | undefined): number | null {
// Should the max number of allowed characters is exceeded, prevent the addition of further tags // Should the max number of allowed characters is exceeded, prevent the addition of further tags
if (this.v$.projectForm.keywords?.$invalid) { if (this.v$.projectForManipulation.keywords?.$invalid) {
return values.length; return values?.length ?? null;
} else { } else {
return null; return null;
} }
}, },
/**
* Add a new tag to the selected keywords
* @param {string} newTag - new tag to be added to the keywords
*/
addTag(newTag: string) { addTag(newTag: string) {
this.selectedKeyword.push(newTag); this.projectForManipulation.keywords?.push(newTag);
this.v$.projectForm.keywords?.$touch(); this.v$.projectForManipulation.keywords?.$touch();
}, },
async retrieveOrganizations(search: string) { async retrieveOrganizations(search: string) {
...@@ -505,76 +510,74 @@ export default defineComponent({ ...@@ -505,76 +510,74 @@ export default defineComponent({
if (this.parentProject) { if (this.parentProject) {
// --- set b-skeleton while filling parent-project form --- // --- set b-skeleton while filling parent-project form ---
this.fillWithProjectMetadata(this.parentProject); this.fillWithProjectMetadata(this.parentProject);
this.getLabels(); this.v$.projectForManipulation.$touch();
this.v$.projectForm.$touch();
} }
}, },
fillWithProjectMetadata(project: ProjectObject) { fillWithProjectMetadata(project: ProjectDto) {
this.projectForm.principleInvestigators = project.principleInvestigators; this.projectForManipulation.principleInvestigators =
this.projectForm.startDate = project.startDate; project.principleInvestigators;
this.projectForm.endDate = project.endDate; this.projectForManipulation.startDate = project.startDate;
this.projectForm.disciplines = project.disciplines; this.projectForManipulation.endDate = project.endDate;
this.projectForm.organizations = project.organizations; this.projectForManipulation.disciplines =
this.projectForm.keywords = project.keywords; project.disciplines?.map((discipline) =>
this.projectForm.visibility = project.visibility; projectMapper.map(
this.projectForm.grantId = project.grantId; DisciplineDto2DisciplineForProjectManipulationDto,
discipline
)
) ?? [];
this.projectForManipulation.organizations =
project.organizations?.map((organization) =>
projectMapper.map(
OrganizationDto2OrganizationForProjectManipulationDto,
organization
)
) ?? [];
this.projectForManipulation.keywords = project.keywords;
this.projectForManipulation.visibility = projectMapper.map(
VisibilityDto2VisibilityForProjectManipulationDto,
project.visibility
);
this.projectForManipulation.grantId = project.grantId;
// Update v-model for Keywords
this.loadKeywords(project);
// Update v-model for Visibility // Update v-model for Visibility
if (project.visibility) { if (project.visibility) {
this.selectedVisibility = project.visibility.id; this.selectedVisibility = project.visibility.id;
} }
}, },
async getLabels() { /**
// Retrieves an organization's displayName * Set visibility for the resource form
/* This method is required because the Organizations are delivered by the Database ProjectModel like so: * @param {string | undefined} visibilityId - ID of the visibility option to be set
displayName:"https://ror.org/04xfq0f34"
url:"https://ror.org/04xfq0f34"
*/ */
this.isLoadingOrganizations = true; setVisibility(visibilityId: string | undefined) {
if (this.projectForm.organizations) { if (visibilityId) {
for (const org of this.projectForm.organizations) { this.$set(this.projectForManipulation, "visibility", {
if (org.url) { id: visibilityId,
const result = await this.projectStore.getOrganizationByURL( } as VisibilityForProjectManipulationDto);
org.url this.v$.projectForManipulation.visibility.$touch();
);
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 | undefined) {
if (this.visibilities && id) {
const visibility = this.visibilities.find(
(entry) => entry.id === id
) as VisibilityObject;
this.projectForm.visibility = visibility;
this.v$.projectForm.visibility?.$touch();
} }
}, },
/**
* Set default visibility option for the resource form
*/
setDefaultVisibility() { setDefaultVisibility() {
if (this.visibilities) { if (this.visibilities) {
const visibility = this.visibilities.find( const visibility = this.visibilities.find(
(entry) => entry.displayName === "Project Members" (entry) => entry.displayName === "Project Members"
) as VisibilityObject; );
if (visibility?.id) {
this.selectedVisibility = visibility.id; this.selectedVisibility = visibility.id;
this.projectForm.visibility = visibility; } else {
// If visibility not found or has no id, select the first visibility from the array this.visibilities
if (this.visibilities.length > 0) {
this.selectedVisibility = this.visibilities[0].id;
}
}
// If selectedVisibility is undefined, assign an empty string
this.setVisibility(this.selectedVisibility);
} }
}, },
}, },
......