diff --git a/.vscode/settings.json b/.vscode/settings.json index 76379b46f9f819d868e0945e50caec7a8d9987af..aa283c20c26eef71b361ddb2021b4db7cb129993 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,7 @@ }, "cSpell.words": [ "Coscine", + "Orcid", "pinia", "RWTH", "vite", diff --git a/src/components/banner/Maintenance.vue b/src/components/banner/Maintenance.vue index 1cf7cba486ec0d59b923730ad6189f8a78eef66a..175e7430047b2eb1c09a7ae3ef22bf56e124010d 100644 --- a/src/components/banner/Maintenance.vue +++ b/src/components/banner/Maintenance.vue @@ -11,9 +11,9 @@ {{ `${messageType}${$t("banner.separator")}` }} </span> {{ messageBody }} - <span v-if="maintenance.url"> + <span v-if="maintenance.href"> {{ $t("banner.maintenance.linkText") }} - <a :href="maintenance.url" target="_blank" + <a :href="maintenance.href" target="_blank" >{{ $t("banner.maintenance.moreInformation") }} </a> </span> diff --git a/src/components/elements/LoadingIndicator.vue b/src/components/elements/LoadingIndicator.vue index 4a14710e6d5f7fbcdbf92d27232e8d955f02f347..038951fb7a154f68052f9c7c795f5e47925ad731 100644 --- a/src/components/elements/LoadingIndicator.vue +++ b/src/components/elements/LoadingIndicator.vue @@ -18,19 +18,20 @@ import useUserStore from "@/modules/user/store"; import { loadingCounterEventHandler } from "@/plugins/loadingCounter"; import { defineComponent } from "vue"; +import type { StoreDefinition } from "pinia"; export default defineComponent({ setup() { const mainStore = useMainStore(); - loadingCounterEventHandler(useAdminStore); - loadingCounterEventHandler(useErrorStore); - loadingCounterEventHandler(useLoginStore); - loadingCounterEventHandler(usePidStore); - loadingCounterEventHandler(useProjectStore); - loadingCounterEventHandler(useResourceStore); - loadingCounterEventHandler(useSearchStore); - loadingCounterEventHandler(useUserStore); + loadingCounterEventHandler(useAdminStore as unknown as StoreDefinition); + loadingCounterEventHandler(useErrorStore as unknown as StoreDefinition); + loadingCounterEventHandler(useLoginStore as unknown as StoreDefinition); + loadingCounterEventHandler(usePidStore as unknown as StoreDefinition); + loadingCounterEventHandler(useProjectStore as unknown as StoreDefinition); + loadingCounterEventHandler(useResourceStore as unknown as StoreDefinition); + loadingCounterEventHandler(useSearchStore as unknown as StoreDefinition); + loadingCounterEventHandler(useUserStore as unknown as StoreDefinition); return { mainStore }; }, diff --git a/src/data/mockup/responses/getMetadata.ts b/src/data/mockup/responses/getMetadata.ts index e9d45b925640bf05a92e87cd738328926207ec5c..1d4c2dd3346dd802df69511cf74ae75330d50391 100644 --- a/src/data/mockup/responses/getMetadata.ts +++ b/src/data/mockup/responses/getMetadata.ts @@ -1,25 +1,29 @@ import type { - FileDto, - MetadataDto, + FileTreeDto, + MetadataTreeDto, TreeDataType, } from "@coscine/api-client/dist/types/Coscine.Api"; -export const getMetadataTreeResponse: MetadataDto[] = [ +export const getMetadataTreeResponse: MetadataTreeDto[] = [ { version: "1693212042", availableVersions: ["1693212042"], - definition: - '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/folder_1/folder_2/A.txt/@type=metadata&version=1693212042>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8477997 ns2:created "2023-08-15"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "Title inside Form Generator";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', - format: "text/turtle", + definition: { + content: + '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/folder_1/folder_2/A.txt/@type=metadata&version=1693212042>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8477997 ns2:created "2023-08-15"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "Title inside Form Generator";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', + type: "text/turtle", + }, path: "folder_1/folder_2/A.txt", type: "Leaf" as TreeDataType.Leaf, }, { version: "1692777419", availableVersions: ["1692777419"], - definition: - '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/file_1.txt/@type=metadata&version=1692777419>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8425359 ns2:created "2023-08-18"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "file_1";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', - format: "text/turtle", + definition: { + content: + '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/file_1.txt/@type=metadata&version=1692777419>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8425359 ns2:created "2023-08-18"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "file_1";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', + type: "text/turtle", + }, path: "file_1.txt", type: "Leaf" as TreeDataType.Leaf, }, @@ -32,25 +36,29 @@ export const getMetadataTreeResponse: MetadataDto[] = [ "1692341031", "1692779210", ], - definition: - '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/file_0.txt/@type=metadata&version=1692779210>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8425361 ns2:created "2023-08-18"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "Revised";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', - format: "text/turtle", + definition: { + content: + '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/file_0.txt/@type=metadata&version=1692779210>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8425361 ns2:created "2023-08-18"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "Revised";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', + type: "text/turtle", + }, path: "file_0.txt", type: "Leaf" as TreeDataType.Leaf, }, { version: "1693209938", availableVersions: ["1693209938"], - definition: - '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/my_folder/file_of_folder.txt/@type=metadata&version=1693209938>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8477996 ns2:created "2023-08-15"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "From Insomnia";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', - format: "text/turtle", + definition: { + content: + '@base <https://purl.org/coscine/resources/4103cbea-ffa3-40a5-9e5c-b99cc16f0007/my_folder/file_of_folder.txt/@type=metadata&version=1693209938>.\r\n\r\n@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.\r\n@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.\r\n@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.\r\n@prefix ns2: <http://purl.org/dc/terms/>.\r\n\r\n_:b8477996 ns2:created "2023-08-15"^^xsd:date;\r\n ns2:creator "Petar Hristov";\r\n ns2:title "From Insomnia";\r\n a <https://purl.org/coscine/ap/base/>.\r\n', + type: "text/turtle", + }, path: "my_folder/file_of_folder.txt", type: "Leaf" as TreeDataType.Leaf, }, ]; -export const getFileTreeResponse: FileDto[] = [ +export const getFileTreeResponse: FileTreeDto[] = [ { - parentDirectory: "", + directory: "", name: "folder_1", size: 0, creationDate: "2023-09-01T16:57:48.2525218+02:00", @@ -59,7 +67,7 @@ export const getFileTreeResponse: FileDto[] = [ type: "Tree" as TreeDataType.Tree, }, { - parentDirectory: "", + directory: "", name: "my_folder", size: 0, creationDate: "2023-09-01T16:57:48.2525245+02:00", @@ -68,7 +76,7 @@ export const getFileTreeResponse: FileDto[] = [ type: "Tree" as TreeDataType.Tree, }, { - parentDirectory: "", + directory: "", name: "file_0.txt", extension: "txt", size: 2513352, diff --git a/src/data/mockup/testLogin.ts b/src/data/mockup/testLogin.ts index 682101e95a31f2f03642f55858c051f9bca43c04..d0679fa5977605667f2db9bfb0529c49781eeeff 100644 --- a/src/data/mockup/testLogin.ts +++ b/src/data/mockup/testLogin.ts @@ -2,9 +2,8 @@ import { type LoginState } from "@/modules/login/types"; import { useLocalStorage } from "@vueuse/core"; export const testLoginState: LoginState = { - currentTosVersion: "1", + currentTosVersion: { version: "1", isCurrent: true }, expiredSession: false, - tosAccepted: { version: ["1"] }, loginStoredData: useLocalStorage("coscine.login.storedData", ""), loginUrls: { orcidUrl: "http://example.org", diff --git a/src/data/mockup/testProject.ts b/src/data/mockup/testProject.ts index b71c3394d13bb83502ee6fce0f3d6340a9f642c9..dc69feb3759d2abf4597d53859604215f8992cb5 100644 --- a/src/data/mockup/testProject.ts +++ b/src/data/mockup/testProject.ts @@ -1,10 +1,13 @@ -import { userMapper, PublicUserDto2ProjectRoleUserDto } from "@/mapping/user"; import { type VisitedProjectDto, type ProjectState, } from "@/modules/project/types"; import type { RoleDto } from "@coscine/api-client/dist/types/Coscine.Api"; -import { testDiscipline, testOrganization, getTestUser } from "./testUser"; +import { + testDiscipline, + testOrganizationFromShibboleth, + getTestShibbolethUser, +} from "./testUser"; export const testOwnerRole: RoleDto = { id: "ownerRole", @@ -26,13 +29,13 @@ export const testSlug = "testProject"; export const testProject: VisitedProjectDto = { id: "987654321", displayName: "Test Project", - projectName: "Test Project Full", + name: "Test Project Full", description: "Test Project Description", principleInvestigators: "Test PI", startDate: "2023-01-01", endDate: "2033-01-01", disciplines: [testDiscipline], - organizations: [testOrganization], + organizations: [testOrganizationFromShibboleth], visibility: { id: "1234", displayName: "Project Members" }, slug: testSlug, invitations: [], @@ -45,13 +48,13 @@ export const testProject: VisitedProjectDto = { ], roles: [ { - projectId: "987654321", + project: { id: "987654321" }, role: testOwnerRole, - user: userMapper.map(PublicUserDto2ProjectRoleUserDto, getTestUser()), + user: getTestShibbolethUser(), }, ], // TODO: Beware the object types! subProjects: [], - creator: getTestUser().id, + creator: { id: getTestShibbolethUser().id }, }; export const testProjectState: ProjectState = { @@ -59,7 +62,7 @@ export const testProjectState: ProjectState = { currentSlug: testSlug, disciplines: [testDiscipline], licenses: [{ id: "452545", displayName: "TestLicense" }], - organizations: [testOrganization], + organizations: [testOrganizationFromShibboleth], roles: [testOwnerRole, testMemberRole, testGuestRole], topLevelProjects: [testProject], visibilities: [ diff --git a/src/data/mockup/testResource.ts b/src/data/mockup/testResource.ts index 9cc50478575fb27873203d20e06e60f8575068c6..ecbf1e2ad6a71b05c975fac24b4fab60a66ba7c9 100644 --- a/src/data/mockup/testResource.ts +++ b/src/data/mockup/testResource.ts @@ -8,7 +8,7 @@ import { baseApplicationProfileFormat, } from "./metadata/applicationProfile"; import { radarFixedValues } from "./metadata/fixedValues"; -import { testDiscipline, getTestUser } from "./testUser"; +import { testDiscipline, getTestShibbolethUser } from "./testUser"; import type { ResourceTypeInformationDto, ResourceTypeStatus, @@ -50,9 +50,9 @@ export const getTestResource: () => Promise<VisitedResourceObject> = apUrl ); const resourceObject: VisitedResourceObject = { - applicationProfile: apUrl, + applicationProfile: { uri: apUrl }, archived: false, - creator: getTestUser().id, + creator: getTestShibbolethUser().id, dateCreated: null, description: "TestResource", disciplines: [testDiscipline], @@ -64,13 +64,13 @@ export const getTestResource: () => Promise<VisitedResourceObject> = license: {}, pid: "21.11102/eeb8d803-46a1-49ba-a47c-81cd4f49cd65", rawApplicationProfile: ap, - resourceName: "TestResource", + name: "TestResource", storedColumns: null, type: { - specificType: testResourceType.specificType, + specificType: testResourceType.specificType ?? undefined, id: testResourceType.id, options: { - gitLabOptions: {}, + gitLab: {}, }, }, usageRights: "", diff --git a/src/data/mockup/testUser.ts b/src/data/mockup/testUser.ts index 31d7b09eca2e49e6435eb95576ff82ffb21a01bc..7986a741e135f6ff5fe72d1ab823d9d139b57034 100644 --- a/src/data/mockup/testUser.ts +++ b/src/data/mockup/testUser.ts @@ -2,48 +2,112 @@ import { type UserState } from "@/modules/user/types"; import type { DisciplineDto, UserDto, + UserInstituteDto, UserOrganizationDto, } from "@coscine/api-client/dist/types/Coscine.Api/api"; -export const testOrganization: UserOrganizationDto = { - name: "TestOrg", - readOnly: true, - rorUri: "example.com", +export const testOrganizationFromShibboleth: UserOrganizationDto = { + displayName: "Test SSO Organization", + readOnly: true, // Shibboleth organizations are always read-only + uri: "example.com", }; -export const testInstitute: UserOrganizationDto = { - name: "TestInstitute", - readOnly: true, - rorUri: "example.com#institute", +export const testInstituteFromShibboleth: UserInstituteDto = { + displayName: "Test SSO Institute", + readOnly: true, // Shibboleth institutes are always read-only + uri: "example.com#institute", }; + +export const testOrganizationFromOrcid: UserOrganizationDto = { + displayName: "Test ORCiD Organization", + readOnly: true, // ORCiD organizations are never read-only + uri: "example.com", +}; +export const testInstituteFromOrcid: UserInstituteDto = { + displayName: "Test ORCiD Institute", + readOnly: true, // ORCiD institutes are never read-only + uri: "example.com#institute", +}; + export const testDiscipline: DisciplineDto = { id: "1", displayNameDe: "Test", displayNameEn: "Test", - url: "example.com", + uri: "example.com", }; export const testLanguage = { id: "1", displayName: "en" }; -export const getTestUser: () => UserDto = () => { +/** + * Generates a mock Shibboleth user data object for testing purposes. + * + * @returns {UserDto} A mock Shibboleth user data object. + */ +export const getTestShibbolethUser: () => UserDto = () => { + return { + id: "d302cb44-c934-4b54-a581-9765cab96fca", + givenName: "Coscine", + familyName: "Example", + displayName: "Coscine Example", + emails: [ + { email: "example@university.com", isConfirmed: true, isPrimary: true }, + ], + disciplines: [testDiscipline], + language: testLanguage, + organizations: [testOrganizationFromShibboleth], + institutes: [testInstituteFromShibboleth], + }; +}; + +/** + * Generates a mock ORCID user data object for testing purposes. + * + * @returns {UserDto} A mock ORCID user data object. + */ +export const getTestOrcidUser: () => UserDto = () => { return { id: "d302cb44-c934-4b54-a581-9765cab96fca", - lastName: "Coscine", - firstName: "Example", + givenName: "Coscine", + familyName: "Example", displayName: "Coscine Example", - email: "example@example.com", + emails: [ + { email: "example@orcid.com", isConfirmed: true, isPrimary: true }, + ], disciplines: [testDiscipline], language: testLanguage, - organizations: [testOrganization], - institutes: [testInstitute], + organizations: [testOrganizationFromOrcid], + institutes: [testInstituteFromOrcid], + }; +}; + +export const getTestShibbolethUserState: () => UserState = () => { + return { + userProfile: { + disciplines: [testDiscipline], + languages: [testLanguage, { id: "2", displayName: "de" }], + organizations: [testOrganizationFromShibboleth], + institutes: [testInstituteFromShibboleth], + titles: [ + { + id: "1", + displayName: "Prof.", + }, + { + id: "2", + displayName: "Dr.", + }, + ], + tokens: null, + }, + user: getTestShibbolethUser(), }; }; -export const getTestUserState: () => UserState = () => { +export const getTestOrcidUserState: () => UserState = () => { return { userProfile: { disciplines: [testDiscipline], languages: [testLanguage, { id: "2", displayName: "de" }], - organizations: [testOrganization], - institutes: [testInstitute], + organizations: [testOrganizationFromShibboleth], + institutes: [testInstituteFromShibboleth], titles: [ { id: "1", @@ -56,6 +120,6 @@ export const getTestUserState: () => UserState = () => { ], tokens: null, }, - user: getTestUser(), + user: getTestOrcidUser(), }; }; diff --git a/src/mapping/project.ts b/src/mapping/project.ts index c3653e3a4263e9d719244d7583d70bbe41f4b1cc..9c229fd2e79252bddd9272715744c5f94ab65be8 100644 --- a/src/mapping/project.ts +++ b/src/mapping/project.ts @@ -24,6 +24,7 @@ export const OrganizationDto2OrganizationForProjectManipulationDto = const configuration = new MapperConfiguration((cfg) => { cfg.createAutoMap(ProjectDto2ProjectForUpdateDto, { + name: (opt) => opt.mapFrom((e) => e.name ?? ""), disciplines: (opt) => opt.mapFromUsing( (e) => e.disciplines, @@ -45,7 +46,9 @@ const configuration = new MapperConfiguration((cfg) => { .createAutoMap(VisibilityDto2VisibilityForProjectManipulationDto, {}) .forSourceMember("displayName", (opt) => opt.ignore()); cfg.createAutoMap(DisciplineDto2DisciplineForProjectManipulationDto, {}); - cfg.createAutoMap(OrganizationDto2OrganizationForProjectManipulationDto, {}); + cfg.createAutoMap(OrganizationDto2OrganizationForProjectManipulationDto, { + uri: (opt) => opt.mapFrom((e) => e.uri ?? ""), + }); }); export const projectMapper = configuration.createMapper(); diff --git a/src/mapping/resource.ts b/src/mapping/resource.ts index bad00b9ffc39b0a88d7d9304b0c40b612f3c330a..a393a43d43fddf165ddc921988fbe76abc638f2b 100644 --- a/src/mapping/resource.ts +++ b/src/mapping/resource.ts @@ -70,6 +70,8 @@ export const ResourceTypeInformationDto2ResourceTypeOptionsForCreationDto = const configuration = new MapperConfiguration((cfg) => { cfg.createAutoMap(ResourceDto2ResourceForUpdateDto, { + name: (opt) => opt.mapFrom((e) => e.name ?? ""), + usageNote: (opt) => opt.mapFrom((e) => e.usageRights), license: (opt) => opt.mapFromUsing( (e) => e.license, @@ -98,31 +100,31 @@ const configuration = new MapperConfiguration((cfg) => { gitlabResourceTypeOptions: (opt) => opt .mapFromUsing( - (e) => e.gitLabOptions, + (e) => e.gitLab, GitLabOptionsDto2GitlabResourceTypeOptionsForUpdateDto ) - .condition((con) => con.gitLabOptions !== undefined), + .condition((con) => con.gitLab !== undefined), rdsResourceTypeOptions: (opt) => opt .mapFromUsing( - (e) => e.rdsOptions, + (e) => e.rds, RdsOptionsDto2RdsResourceTypeOptionsForManipulationDto ) - .condition((con) => con.rdsOptions !== undefined), + .condition((con) => con.rds !== undefined), rdsS3ResourceTypeOptions: (opt) => opt .mapFromUsing( - (e) => e.rdsS3Options, + (e) => e.rdsS3, RdsS3OptionsDto2RdsS3ResourceTypeOptionsForManipulationDto ) - .condition((con) => con.rdsS3Options !== undefined), + .condition((con) => con.rdsS3 !== undefined), rdsS3WormResourceTypeOptions: (opt) => opt .mapFromUsing( - (e) => e.rdsS3WormOptions, + (e) => e.rdsS3Worm, RdsS3WormOptionsDto2RdsS3WormResourceTypeOptionsForManipulationDto ) - .condition((con) => con.rdsS3WormOptions !== undefined), + .condition((con) => con.rdsS3Worm !== undefined), }); cfg.createMap(ResourceTypeInformationDto2ResourceTypeOptionsForCreationDto, { linkedResourceTypeOptions: (opt) => @@ -176,20 +178,20 @@ const configuration = new MapperConfiguration((cfg) => { }); cfg.createAutoMap(GitLabOptionsDto2GitlabResourceTypeOptionsForUpdateDto, {}); cfg.createAutoMap(RdsOptionsDto2RdsResourceTypeOptionsForManipulationDto, { - size: (opt) => + quota: (opt) => opt.mapFromUsing((e) => e.size, QuotaDto2QuotaForManipulationDto), }); cfg.createAutoMap( RdsS3OptionsDto2RdsS3ResourceTypeOptionsForManipulationDto, { - size: (opt) => + quota: (opt) => opt.mapFromUsing((e) => e.size, QuotaDto2QuotaForManipulationDto), } ); cfg.createAutoMap( RdsS3WormOptionsDto2RdsS3WormResourceTypeOptionsForManipulationDto, { - size: (opt) => + quota: (opt) => opt.mapFromUsing((e) => e.size, QuotaDto2QuotaForManipulationDto), } ); diff --git a/src/mapping/tree.ts b/src/mapping/tree.ts index 15084f64009a31a773e370b456f3be6c3f89aa30..dc3439feebbf1f67a8f87870b06794229aa70b9d 100644 --- a/src/mapping/tree.ts +++ b/src/mapping/tree.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import type { TreeDataType, - FileDto, + FileTreeDto, } from "@coscine/api-client/dist/types/Coscine.Api"; import type { FileInformation, @@ -11,12 +11,12 @@ import type { } from "@/modules/resource/types"; export const TreeDto2FileInformation = new MappingPair< - FileDto, + FileTreeDto, FileInformation >(); export const TreeDto2FolderInformation = new MappingPair< - FileDto, + FileTreeDto, FolderInformation >(); @@ -24,7 +24,7 @@ const configuration = new MapperConfiguration((cfg) => { cfg.createMap(TreeDto2FileInformation, { id: (opt) => opt.mapFrom((_) => uuidv4()), name: (opt) => opt.mapFrom((dto) => dto.name ?? ""), - parentDirectory: (opt) => opt.mapFrom((dto) => dto.parentDirectory ?? ""), + parentDirectory: (opt) => opt.mapFrom((dto) => dto.directory ?? ""), path: (opt) => opt.mapFrom((dto) => dto.path ?? ""), type: (opt) => opt.mapFrom((_) => "Leaf" as TreeDataType.Leaf), isFolder: (opt) => opt.mapFrom((_) => false), @@ -38,7 +38,7 @@ const configuration = new MapperConfiguration((cfg) => { cfg.createMap(TreeDto2FolderInformation, { id: (opt) => opt.mapFrom((_) => uuidv4()), name: (opt) => opt.mapFrom((dto) => dto.name ?? ""), - parentDirectory: (opt) => opt.mapFrom((dto) => dto.parentDirectory ?? ""), + parentDirectory: (opt) => opt.mapFrom((dto) => dto.directory ?? ""), path: (opt) => opt.mapFrom((dto) => dto.path ?? ""), type: (opt) => opt.mapFrom((_) => "Tree" as TreeDataType.Tree), isFolder: (opt) => opt.mapFrom((_) => true), diff --git a/src/mapping/user.ts b/src/mapping/user.ts index be4394fba71c19f289ff28dc24c519b1cd25162c..2e6b61123768ae3c243c21b8df6d2d28713acdc9 100644 --- a/src/mapping/user.ts +++ b/src/mapping/user.ts @@ -5,18 +5,12 @@ import type { 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 @@ -35,16 +29,11 @@ export const LanguageDto2LanguageForUserManipulationDto = new MappingPair< >(); 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(), + givenName: (opt) => opt.mapFrom((dto) => dto.givenName ?? ""), + familyName: (opt) => opt.mapFrom((dto) => dto.familyName ?? ""), + email: (opt) => + opt.mapFrom((dto) => dto.emails?.find((e) => e.isPrimary)?.email ?? ""), title: (opt) => opt .condition((dto) => undefined !== dto.title) @@ -61,11 +50,11 @@ const configuration = new MapperConfiguration((cfg) => { ), institute: (opt) => opt.mapFrom((dto) => - dto.institutes?.length ? dto.institutes[0].name : "" + dto.institutes?.length ? dto.institutes[0].displayName : "" ), organization: (opt) => opt.mapFrom((dto) => - dto.organizations?.length ? dto.organizations[0].name : null + dto.organizations?.length ? dto.organizations[0].displayName : null ), }); cfg.createAutoMap(TitleDto2TitleForUserManipulationDto, {}); diff --git a/src/modules/login/store.ts b/src/modules/login/store.ts index 6a0a3452520d1482cbbb75f2feba3f33ab2c333a..a739ef61ace5cebfcb6d1aa18c26e61804d58b96 100644 --- a/src/modules/login/store.ts +++ b/src/modules/login/store.ts @@ -8,10 +8,11 @@ import type { DFNAAIData, DFNAAIInstitution, LoginState } from "./types"; import useMainStore from "@/store/index"; import useNotificationStore from "@/store/notification"; import { useLocalStorage } from "@vueuse/core"; -import { AccountApi, ToSApi, UserApi } from "@coscine/api-client"; +import { AccountApi, SelfApi, TosApi } from "@coscine/api-client"; import type { AxiosError } from "axios"; import axios from "axios"; import type { LoginUrls } from "@coscine/api-client/dist/types/Coscine.Api.STS"; +import { UserTermsOfServiceAcceptDto } from "@coscine/api-client/dist/types/Coscine.Api"; /* Store variable name is "this.<id>Store" @@ -145,7 +146,7 @@ export const useLoginStore = defineStore({ async retrieveCurrentTosVersion() { const notificationStore = useNotificationStore(); try { - const apiResponse = await ToSApi.getToS(); + const apiResponse = await TosApi.getTos(); this.currentTosVersion = apiResponse.data.data; } catch (error) { // Handle other Status Codes @@ -171,7 +172,15 @@ export const useLoginStore = defineStore({ async acceptToS(): Promise<boolean> { const notificationStore = useNotificationStore(); try { - await UserApi.acceptCurrentToS(); + // Retrieve the current TOS version if missing + if (!this.currentTosVersion?.version) { + await this.retrieveCurrentTosVersion(); + } + // Build the DTO and send it + const userTermsOfServiceAcceptDto: UserTermsOfServiceAcceptDto = { + version: this.currentTosVersion?.version ?? "", + }; + await SelfApi.acceptCurrentTos(userTermsOfServiceAcceptDto); return true; } catch (error) { // Handle other Status Codes diff --git a/src/modules/pid/pages/Pid.vue b/src/modules/pid/pages/Pid.vue index 9c4baa200299cb48f8884c24dce28de6147e2ea8..c8622f41cdb64cc204e716dba7e819a38d8a1f58 100644 --- a/src/modules/pid/pages/Pid.vue +++ b/src/modules/pid/pages/Pid.vue @@ -149,7 +149,7 @@ import usePidStore from "../store"; import useNotificationStore from "@/store/notification"; import { useVuelidate, type ValidationArgs } from "@vuelidate/core"; import { email, maxLength, required } from "@vuelidate/validators"; -import type { PidEnquiryDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; +import type { PidRequestDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; export default defineComponent({ setup() { @@ -168,7 +168,7 @@ export default defineComponent({ email: "", message: "", sendConfirmationEmail: true, - } as PidEnquiryDto, + } as PidRequestDto, isLoading: false, isPidValid: null as boolean | null, }; @@ -190,7 +190,7 @@ export default defineComponent({ email: { email, required }, message: { required, maxLength: maxLength(5000) }, sendConfirmationEmail: { required }, - } as ValidationArgs<PidEnquiryDto>, + } as ValidationArgs<PidRequestDto>, }; }, diff --git a/src/modules/pid/store.ts b/src/modules/pid/store.ts index b88c1e36c7293d5af0197036a4a3826f9cbd7658..7bb0e4e56d464248246cf170a86f5c0c1bda6238 100644 --- a/src/modules/pid/store.ts +++ b/src/modules/pid/store.ts @@ -5,7 +5,7 @@ import useNotificationStore from "@/store/notification"; import { PidApi } from "@coscine/api-client"; import { AxiosError } from "axios"; import { StatusCodes } from "http-status-codes"; -import type { PidEnquiryDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; +import type { PidRequestDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; /* Store variable name is "this.<id>Store" @@ -64,20 +64,20 @@ 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. + * @param {PidRequestDto} pidRequestDto - The PID request 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( prefix: string, id: string, - pidEnquiryDto: PidEnquiryDto + pidRequestDto: PidRequestDto ): Promise<boolean | null> { const notificationStore = useNotificationStore(); try { - const apiResponse = await PidApi.sendEmailToOwner( + const apiResponse = await PidApi.sendRequestToOwner( prefix, id, - pidEnquiryDto + pidRequestDto ); // Note: Beware that only 204 (No Content) is considered a success in this implementation. return apiResponse.status === StatusCodes.NO_CONTENT ? true : false; diff --git a/src/modules/project/i18n/de.ts b/src/modules/project/i18n/de.ts index 2d5b7b50a934e4d6ac85c70fe79e96ccca8f981d..d590cb7aef93759d07d8197a562561542d27696f 100644 --- a/src/modules/project/i18n/de.ts +++ b/src/modules/project/i18n/de.ts @@ -45,8 +45,8 @@ export default { "Sie können eine Einladung an diese E-Mail Adresse senden", alreadyGotRole: "(bereits Mitglied)", selectRolePlaceholder: "Rolle auswählen", - firstName: "Vorname", - lastName: "Nachname", + givenName: "Vorname", + familyName: "Nachname", email: "E-Mail", actions: "Aktionen", status: "Status", diff --git a/src/modules/project/i18n/en.ts b/src/modules/project/i18n/en.ts index 50d55151bea89652b8d492cdbef09bdf85f25099..4c6e5e77af39db53095cf6dbfe3745dff8c8dad6 100644 --- a/src/modules/project/i18n/en.ts +++ b/src/modules/project/i18n/en.ts @@ -42,8 +42,8 @@ export default { searchEmailInvite: "You may send an invitation to this email address", alreadyGotRole: "(Already added)", selectRolePlaceholder: "Select Role", - firstName: "First Name", - lastName: "Last Name", + givenName: "Given Name", + familyName: "Family Name", email: "Email", actions: "Actions", status: "Status", diff --git a/src/modules/project/pages/ConfigurationMetadata.spec.ts b/src/modules/project/pages/ConfigurationMetadata.spec.ts index e5f2d615f5cb8fe2fe360f1256a2c014e04d0124..00b39509df89acac3ed74a5a510475cb00ca9c97 100644 --- a/src/modules/project/pages/ConfigurationMetadata.spec.ts +++ b/src/modules/project/pages/ConfigurationMetadata.spec.ts @@ -21,7 +21,7 @@ import type Vue from "vue"; /* Import of relevant mockup data */ import { testProjectState } from "@/data/mockup/testProject"; -import { getTestUserState } from "@/data/mockup/testUser"; +import { getTestShibbolethUserState } from "@/data/mockup/testUser"; /* Create a local Vue instance */ const localVue = createLocalVue(); @@ -31,7 +31,7 @@ describe("ConfigurationMetadata.vue", () => { /* Describe Pre-initialization steps */ /* Description of the test */ - test("should enable buttons and update project name", async () => { + test("Should enable buttons and update project name", async () => { /* Test Pre-initialization steps */ /* Mount the Component */ @@ -40,7 +40,7 @@ describe("ConfigurationMetadata.vue", () => { createSpy: vitest.fn, initialState: { project: testProjectState, - user: getTestUserState(), + user: getTestShibbolethUserState(), }, }), i18n, @@ -48,31 +48,29 @@ describe("ConfigurationMetadata.vue", () => { }); // Check initial state of buttons - expect(wrapper.get("#DeleteProjectBtn").attributes()["disabled"]).not.toBe( - "disabled" - ); // Delete button - active - expect(wrapper.get("#SubmitProjectBtn").attributes()["disabled"]).toBe( - "disabled" - ); // Submit button - disabled + expect( + (wrapper.get("#DeleteProjectBtn").element as HTMLButtonElement).disabled + ).toBeFalsy(); // Delete button - active + expect( + (wrapper.get("#SubmitProjectBtn").element as HTMLButtonElement).disabled + ).toBeTruthy(); // Submit button - disabled await wrapper.vm.$nextTick(); // Find element (Project Name) const element = wrapper.get("#ProjectName"); - expect(element.exists()).toBe(true); + expect(element.exists()).toBeTruthy(); // Change value of element await element.setValue("New Test Project"); - expect(wrapper.vm.$data.projectForUpdate.projectName).toBe( - "New Test Project" - ); + expect(wrapper.vm.$data.projectForUpdate.name).toBe("New Test Project"); // Buttons should be enabled - expect(wrapper.get("#DeleteProjectBtn").attributes()["disabled"]).not.toBe( - "disabled" - ); // Delete button - active - expect(wrapper.get("#SubmitProjectBtn").attributes()["disabled"]).not.toBe( - "disabled" - ); // Submit button - active + expect( + (wrapper.get("#DeleteProjectBtn").element as HTMLButtonElement).disabled + ).toBeFalsy(); // Delete button - active + expect( + (wrapper.get("#SubmitProjectBtn").element as HTMLButtonElement).disabled + ).toBeFalsy(); // Submit button - active }); }); diff --git a/src/modules/project/pages/ConfigurationMetadata.vue b/src/modules/project/pages/ConfigurationMetadata.vue index 2a638a1097edb03dc4165b463176b4a6cebdd9e9..f39cae2fbf07b972417625858fd2940a385f24a4 100644 --- a/src/modules/project/pages/ConfigurationMetadata.vue +++ b/src/modules/project/pages/ConfigurationMetadata.vue @@ -121,7 +121,7 @@ export default defineComponent({ startDate: "", endDate: "", keywords: [] as string[], - projectName: "", + name: "", principleInvestigators: "", grantId: "", slug: "", diff --git a/src/modules/project/pages/CreateProject.vue b/src/modules/project/pages/CreateProject.vue index 4b0206e347937cac3d0bd217ca289c84989492dd..848e37ffd351249eb42d826e867099683956374b 100644 --- a/src/modules/project/pages/CreateProject.vue +++ b/src/modules/project/pages/CreateProject.vue @@ -103,7 +103,7 @@ export default defineComponent({ startDate: moment(new Date()).format("YYYY-MM-DD"), endDate: moment(new Date()).format("YYYY-MM-DD"), keywords: [] as string[], - projectName: "", + name: "", principleInvestigators: "", grantId: "", disciplines: [], @@ -126,7 +126,7 @@ export default defineComponent({ const memberships = this.userStore.user?.organizations; if (memberships) { return memberships.some( - (membership) => membership.rorUri === "https://ror.org/04xfq0f34" + (membership) => membership.uri === "https://ror.org/04xfq0f34" ); } return false; diff --git a/src/modules/project/pages/ListProjects.vue b/src/modules/project/pages/ListProjects.vue index 3c48233bf46be68ac2eba73f03ff9900ba906b8c..b3710c6d512d7ae20c318ff39b2b3bf654a44c12 100644 --- a/src/modules/project/pages/ListProjects.vue +++ b/src/modules/project/pages/ListProjects.vue @@ -23,9 +23,9 @@ </template> <script lang="ts"> -import type { ProjectDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; import { defineComponent } from "vue"; import type { RawLocation } from "vue-router"; +import type { ProjectDto } from "@coscine/api-client/dist/types/Coscine.Api/api"; // import the store for current module import useProjectStore from "../store"; diff --git a/src/modules/project/pages/Members.vue b/src/modules/project/pages/Members.vue index 767e1c0b94c6f30ca4c41b4b5e6c004045cb0e8e..5dfd9252198e7e169e1451bd2e77ae41a2dbde61 100644 --- a/src/modules/project/pages/Members.vue +++ b/src/modules/project/pages/Members.vue @@ -72,7 +72,7 @@ description-key="page.members.removeSelectedInvitation" title-key="page.members.deleteInvitationTitle" :visible="isDeleteInvitedUserModalVisible" - :selected-user="selectedInvitation.userMail" + :selected-user="selectedInvitation.email" :selected-project="projectName" @ok="confirmRevokeInvitation" @close="closeInvitedUserModal" @@ -227,18 +227,18 @@ export default defineComponent({ // header text react on language changes. return [ { - label: this.$t("page.members.firstName"), - key: "user.firstName", + label: this.$t("page.members.givenName"), + key: "givenName", sortable: true, }, { - label: this.$t("page.members.lastName"), - key: "user.lastName", + label: this.$t("page.members.familyName"), + key: "familyName", sortable: true, }, { label: this.$t("page.members.email"), - key: "user.emailAddress", + key: "email", sortable: true, }, { @@ -310,16 +310,16 @@ export default defineComponent({ resendInvitation(selectedInvitation: ProjectInvitationDto) { const invitation: ProjectInvitationForProjectManipulationDto = { - roleId: selectedInvitation.roleId ?? "", - email: selectedInvitation.userMail ?? "", + roleId: selectedInvitation.role?.id ?? "", + email: selectedInvitation.email ?? "", }; const errorText = this.$t("page.members.invitedUserError", { - email: selectedInvitation.userMail, + email: selectedInvitation.email, }).toString(); const text = this.$t("page.members.invitedUserText", { - email: selectedInvitation.userMail, - role: selectedInvitation.roleId - ? this.getRoleNameFromId(selectedInvitation.roleId) + email: selectedInvitation.email, + role: selectedInvitation.role?.id + ? this.getRoleNameFromId(selectedInvitation.role.id) : "", projectName: this.projectName, }).toString(); @@ -332,17 +332,15 @@ export default defineComponent({ }, revokeInvitation(selectedInvitation: ProjectInvitationDto) { - this.selectedInvitation.id = selectedInvitation.id; - this.selectedInvitation.userMail = selectedInvitation.userMail; - this.selectedInvitation.projectId = selectedInvitation.projectId; - + // Clone the object to avoid changing the original object + this.selectedInvitation = structuredClone(selectedInvitation); this.isDeleteInvitedUserModalVisible = true; }, async confirmRevokeInvitation() { if (this.selectedInvitation.id) { const text = this.$t("page.members.removedUser", { - email: this.selectedInvitation.userMail, + email: this.selectedInvitation.email, project: this.projectName, }).toString(); const worked = await this.projectStore.deleteInvitation( @@ -356,7 +354,7 @@ export default defineComponent({ }); } else { const errorMsg = this.$t("page.members.deleteExternalUserError", { - email: this.selectedInvitation.userMail, + email: this.selectedInvitation.email, }).toString(); this.notificationStore.postNotification({ title: this.$t("page.members.userManagement").toString(), @@ -381,7 +379,7 @@ export default defineComponent({ async setRole(projectRole: ProjectRoleDto) { if (this.project && this.project.id && projectRole.id) { - projectRole.projectId = this.project.id; + projectRole.project = { id: this.project.id }; if (projectRole.role) { projectRole.role.displayName = @@ -431,7 +429,7 @@ export default defineComponent({ async addUser(projectRole: ProjectRoleDto, callback?: () => void) { if (this.project && this.project.id) { - projectRole.projectId = this.project.id; + projectRole.project = { id: this.project.id }; const text = this.$t("page.members.addedUser", { project: this.projectName, @@ -447,7 +445,7 @@ export default defineComponent({ await this.projectStore.addMembershipToProject(this.project.id, { roleId: projectRole.role?.id, - userId: projectRole.user?.userId, + userId: projectRole.user?.id, } as ProjectRoleForProjectCreationDto); await this.getProjectRoles(); this.notificationStore.postNotification({ @@ -464,20 +462,20 @@ export default defineComponent({ this.candidateForInvitation = { projectId: this.project?.id ?? "", roleId: projectRole.role?.id ?? "", - email: projectRole.user?.emailAddress ?? "", + email: projectRole.user?.email ?? "", expired: false, invited: false, }; if (this.invitations) { for (const invitation of this.invitations) { if ( - invitation.userMail?.toLowerCase() === + invitation.email?.toLowerCase() === projectRole.user?.displayName?.toLowerCase() && - invitation.expiration + invitation.expirationDate ) { this.candidateForInvitation.invited = true; const today = new Date(); - const expirationDate = new Date(invitation.expiration); + const expirationDate = new Date(invitation.expirationDate); if (expirationDate <= today) { this.candidateForInvitation.expired = true; } diff --git a/src/modules/project/pages/ProjectPage.vue b/src/modules/project/pages/ProjectPage.vue index 9a024eafc5606f50026535391edfe92a344b7e41..82fde69430a370b7809a68ebc6b20327866c8046 100644 --- a/src/modules/project/pages/ProjectPage.vue +++ b/src/modules/project/pages/ProjectPage.vue @@ -141,7 +141,7 @@ export default defineComponent({ return this.projectStore.currentResources; }, isEmailValid(): boolean { - return this.user?.isEmailConfirmed ?? false; + return this.user?.emails?.at(0)?.isConfirmed ?? false; }, isGuest(): boolean | undefined { return this.projectStore.currentUserRoleIsGuest; diff --git a/src/modules/project/pages/Quota.vue b/src/modules/project/pages/Quota.vue index f705e9bb18c0a8e71d1b906e38aa628cb97c0eb8..838336e7b361686e7e70748811503b9a404fc8df 100644 --- a/src/modules/project/pages/Quota.vue +++ b/src/modules/project/pages/Quota.vue @@ -187,7 +187,10 @@ <!-- Resource Name Cell Contents --> <template #cell(name)="data"> - {{ data.item.resourceDisplayName }} + {{ + resources?.find((r) => r.id === data.item.resource?.id) + ?.displayName + }} <b-badge v-if="isArchived(data.item.resourceId)" pill @@ -465,7 +468,7 @@ export default defineComponent({ ): Promise<void> { if ( this.project?.id && - resourceQuotaDto.resourceId && + resourceQuotaDto.resource?.id && resourceQuotaDto.reserved?.value && resourceQuotaDto.reserved?.unit ) { @@ -478,7 +481,7 @@ export default defineComponent({ const success = await this.projectStore.updateResourceQuota( this.project.id, - resourceQuotaDto?.resourceId, + resourceQuotaDto?.resource.id, desiredReservedResourceQuota ); @@ -491,7 +494,9 @@ export default defineComponent({ "page.quota.resourceQuotaChangedSuccessTitle" ).toString(), body: this.$t("page.quota.resourceQuotaChangedSuccessBody", { - ResourceName: resourceQuotaDto.resourceDisplayName, + ResourceName: this.resources?.find( + (r) => r.id === resourceQuotaDto.resource?.id + )?.displayName, AmountInGB: this.toGiB(resourceQuotaDto.reserved), }).toString(), }); @@ -556,8 +561,8 @@ export default defineComponent({ minGiB(resourceId: string): number { // To get the used quota, use the information in the store. const resourceQuota = this.resourceTypesQuotas - ?.find((rtq) => rtq.resourceTypeId === this.selectedResourceTypeId) - ?.resourceQuotas?.find((rq) => rq.resourceId === resourceId); + ?.find((rtq) => rtq.resourceType?.id === this.selectedResourceTypeId) + ?.resourceQuotas?.find((rq) => rq.resource?.id === resourceId); const usedQuotaInGiB = this.toGiB(resourceQuota?.used); return Math.ceil(usedQuotaInGiB || 1); @@ -573,8 +578,8 @@ export default defineComponent({ // IMPORTANT: Read the summary above cloneSelectedQuotas. // If done differently this method breaks and the slider can increase infinitely. const resourceQuota = this.resourceTypesQuotas - ?.find((rtq) => rtq.resourceTypeId === this.selectedResourceTypeId) - ?.resourceQuotas?.find((rq) => rq.resourceId === resourceId); + ?.find((rtq) => rtq.resourceType?.id === this.selectedResourceTypeId) + ?.resourceQuotas?.find((rq) => rq.resource?.id === resourceId); // Read the current reserved quota in the store const reservedQuotaInGiB = this.toGiB(resourceQuota?.reserved); // Calculate the free quota available for increasing this resource's reserved quota @@ -596,7 +601,7 @@ export default defineComponent({ cloneSelectedQuotas() { this.selectedQuotas = _.cloneDeep( this.resourceTypesQuotas?.find( - (rtq) => rtq.resourceTypeId === this.selectedResourceTypeId + (rtq) => rtq.resourceType?.id === this.selectedResourceTypeId ) ); }, diff --git a/src/modules/project/pages/components/FormMetadata.vue b/src/modules/project/pages/components/FormMetadata.vue index d7489c613e36552642d222f50cd6ff848cbbf031..f4738c680147d0818d5e6e3ec4edfad20cc87269 100644 --- a/src/modules/project/pages/components/FormMetadata.vue +++ b/src/modules/project/pages/components/FormMetadata.vue @@ -7,12 +7,11 @@ label-for="CopyData" :label=" $t('form.project.copyMetadataLabel', { - project: parentProject.projectName, + project: parentProject.name, }) " :is-loading="isLoading" type="button" - class="d-flex align-items-center" > <b-button id="project_copy_button" @@ -21,8 +20,8 @@ :disabled="disabled" @click.prevent="copyMetadataFromParent" > - {{ $t("buttons.copyMetadata") }}</b-button - > + {{ $t("buttons.copyMetadata") }} + </b-button> </CoscineFormGroup> <!-- Principal Investigators --> @@ -177,7 +176,7 @@ :multiple="true" :loading="isLoadingOrganizations" :hide-selected="true" - label="name" + label="displayName" track-by="rorUri" :show-labels="false" :placeholder="$t('form.project.projectOrganization')" diff --git a/src/modules/project/pages/components/FormNaming.vue b/src/modules/project/pages/components/FormNaming.vue index ee22a542434349c971e4908bd182372440c6dbc6..f4c533510d72d73385524facffd86e3715c52012 100644 --- a/src/modules/project/pages/components/FormNaming.vue +++ b/src/modules/project/pages/components/FormNaming.vue @@ -10,14 +10,14 @@ > <b-form-input id="ProjectName" - v-model="v$.projectForManipulation.projectName.$model" + v-model="v$.projectForManipulation.name.$model" :state=" - v$.projectForManipulation.projectName.$dirty - ? !v$.projectForManipulation.projectName.$error + v$.projectForManipulation.name.$dirty + ? !v$.projectForManipulation.name.$error : null " :placeholder="$t('form.project.projectName')" - :maxlength="v$.projectForManipulation.projectName.maxLength.$params.max" + :maxlength="v$.projectForManipulation.name.maxLength.$params.max" required :disabled="disabled" @input="translateProjectNameToDisplayName" @@ -25,8 +25,7 @@ <div class="invalid-tooltip"> {{ $t("form.project.projectNameHelp", { - maxLength: - v$.projectForManipulation.projectName.maxLength.$params.max, + maxLength: v$.projectForManipulation.name.maxLength.$params.max, }) }} </div> @@ -137,7 +136,7 @@ export default defineComponent({ }); const rules = { projectForManipulation: { - projectName: { required, maxLength: maxLength(200) }, + name: { required, maxLength: maxLength(200) }, displayName: { required, maxLength: maxLength(25) }, description: { required, maxLength: maxLength(5000) }, }, @@ -164,12 +163,9 @@ export default defineComponent({ methods: { translateProjectNameToDisplayName() { - if ( - !this.isLockedDisplayName && - this.projectForManipulation.projectName - ) { + if (!this.isLockedDisplayName && this.projectForManipulation.name) { this.projectForManipulation.displayName = - this.projectForManipulation.projectName.substring(0, 25); + this.projectForManipulation.name.substring(0, 25); this.v$.projectForManipulation.displayName?.$touch(); } }, diff --git a/src/modules/project/pages/components/MembersList.vue b/src/modules/project/pages/components/MembersList.vue index d985db4870f98dfffd5f8674f332bd601081a29a..fe0de06a0f81561405e6532acc0b4bad4288ed84 100644 --- a/src/modules/project/pages/components/MembersList.vue +++ b/src/modules/project/pages/components/MembersList.vue @@ -21,7 +21,7 @@ v-if=" projectRole.user && currentUser && - projectRole.user.userId === currentUser.id + projectRole.user.id === currentUser.id " class="badge badge-pill badge-primary ml-2" > @@ -34,17 +34,17 @@ <!-- Member Email --> <a v-b-tooltip.hover.bottom="{ - title: projectRole.user ? projectRole.user.emailAddress : '', + title: projectRole.user ? projectRole.user.email : '', boundary: 'viewport', }" class="text-primary" :href=" projectRole.user - ? `mailto:${projectRole.user.emailAddress}` + ? `mailto:${projectRole.user.email}` : undefined " > - {{ projectRole.user ? projectRole.user.emailAddress : "" }} + {{ projectRole.user ? projectRole.user.email : "" }} </a> </b-col> @@ -62,7 +62,7 @@ v-if=" projectRole.user && currentUser && - projectRole.user.userId === currentUser.id + projectRole.user.id === currentUser.id " class="float-right" variant="primary" @@ -152,15 +152,22 @@ export default defineComponent({ }, computed: { - currentUser(): UserDto | null | undefined { - return this.userStore.user; - }, project(): ProjectDto | null { return this.projectStore.currentProject; }, projectRoles(): ProjectRoleDto[] | null { return this.projectStore.currentProjectRolesSorted; }, + currentUser(): UserDto | null | undefined { + return this.userStore.user; + }, + currentUserProjectRole(): ProjectRoleDto | undefined { + return this.projectRoles?.find( + (r) => + r.user?.id === this.currentUser?.id && + r.project?.id === this.project?.id + ); + }, isOwner(): boolean | undefined { return this.projectStore.currentUserRoleIsOwner; }, @@ -180,7 +187,10 @@ export default defineComponent({ methods: { leaveProject() { if (this.project) { - this.projectStore.deleteProjectAssociation(this.project, null); + this.projectStore.deleteProjectAssociation( + this.project, + this.currentUserProjectRole + ); } }, }, diff --git a/src/modules/project/pages/components/MembersTable.vue b/src/modules/project/pages/components/MembersTable.vue index 7873fe0496939f07e89730fa9cc2ad9f6bfab388..3019dfe1b13022abead971e255c87bf8fca49925 100644 --- a/src/modules/project/pages/components/MembersTable.vue +++ b/src/modules/project/pages/components/MembersTable.vue @@ -29,6 +29,21 @@ <strong class="ml-2">{{ $t("default.loading") }}</strong> </div> + <!-- Project Members - Given Name Column --> + <template #cell(givenName)="row"> + {{ row.item.user.givenName }} + </template> + + <!-- Project Members - Family Name Column --> + <template #cell(familyName)="row"> + {{ row.item.user.familyName }} + </template> + + <!-- Project Members - Email Column --> + <template #cell(email)="row"> + {{ row.item.user.email }} + </template> + <!-- Project Members - Role Column --> <template #cell(role)="row"> <div v-if="row.item.role" class="noOverflow"> @@ -48,10 +63,15 @@ </div> </template> + <!-- Invited Users - Email Column --> + <template #cell(userMail)="row"> + {{ row.item.email }} + </template> + <!-- Invited Users - Role Column --> <template #cell(roleId)="row"> <div class="noOverflow"> - {{ getRoleNameFromId(row.item.roleId) }} + {{ getRoleNameFromId(row.item.role.id) }} </div> </template> @@ -80,10 +100,10 @@ <template #cell(expiration)="row"> <div :class="{ - 'text-danger': checkExpiration(row.item.expiration), + 'text-danger': isInvitationExpired(row.item.expirationDate), }" > - {{ getStatus(row.item.expiration) }} + {{ getInvitationStatus(row.item.expirationDate) }} </div> </template> @@ -97,16 +117,18 @@ variant="danger" :btn-text="deleteText" @click="revokeInvitation(selectedInvitation.item)" - >{{ deleteText }}</b-button > + {{ deleteText }} + </b-button> <!-- Resend Invitation --> <b-button class="rightActionBtn" size="sm" - :disabled="!checkExpiration(selectedInvitation.item.expiration)" + :disabled="!isInvitationExpired(selectedInvitation.item.expiration)" @click="resendInvitation(selectedInvitation.item)" - >{{ $t("buttons.resend") }} + > + {{ $t("buttons.resend") }} </b-button> </div> </template> @@ -201,14 +223,9 @@ export default defineComponent({ role(item: ProjectRoleDto) { this.$emit("setRole", item); }, - checkExpiration(expiration: string) { + isInvitationExpired(expirationDateString: string) { const today = new Date(); - const expirationDate = new Date(expiration); - if (expirationDate > today) { - return false; - } else { - return true; - } + return new Date(expirationDateString) <= today; }, getRoleNameFromId(roleId: string): string | null | undefined { if (this.roles) { @@ -221,8 +238,8 @@ export default defineComponent({ return null; } }, - getStatus(expiration: string) { - if (this.checkExpiration(expiration)) { + getInvitationStatus(expirationDate: string) { + if (this.isInvitationExpired(expirationDate)) { return this.$t("page.members.expiredStatus"); } else { return this.$t("page.members.pendingStatus"); diff --git a/src/modules/project/pages/components/UserSearchRow.vue b/src/modules/project/pages/components/UserSearchRow.vue index 4feeb7c9cf83fbb35b50462792177cc44fe7aabc..9b23945f1ed18ce72899b809bc8a36fce6f499cb 100644 --- a/src/modules/project/pages/components/UserSearchRow.vue +++ b/src/modules/project/pages/components/UserSearchRow.vue @@ -146,11 +146,9 @@ import useUserStore from "@/modules/user/store"; import type { ProjectDto, ProjectRoleDto, - ProjectRoleUserDto, PublicUserDto, RoleDto, } from "@coscine/api-client/dist/types/Coscine.Api/api"; -import { PublicUserDto2ProjectRoleUserDto, userMapper } from "@/mapping/user"; export default defineComponent({ props: { @@ -169,7 +167,7 @@ export default defineComponent({ projectMembers: { default: undefined, type: [Array, undefined] as PropType< - (ProjectRoleUserDto | undefined)[] | undefined + (PublicUserDto | undefined)[] | undefined >, }, isOwner: { @@ -185,7 +183,7 @@ export default defineComponent({ data() { return { newUserRole: { - projectId: "", + project: { id: "" }, role: { id: "" }, user: {}, } as ProjectRoleDto, @@ -193,7 +191,7 @@ export default defineComponent({ queriedUsers: [] as PublicUserDto[], searchString: "", selectableCheck: (user: PublicUserDto) => - !this.projectMembers?.some((pm) => pm?.userId === user.id), + !this.projectMembers?.some((u) => u?.id === user.id), selectedAddingUser: null as PublicUserDto | null, }; }, @@ -214,10 +212,10 @@ export default defineComponent({ }, emailInvitation(): boolean { if ( - this.newUserRole?.projectId && + this.newUserRole?.project?.id && this.newUserRole?.role?.id && - !this.newUserRole?.user?.userId && - this.newUserRole?.user?.emailAddress + !this.newUserRole?.user?.id && + this.newUserRole?.user?.email ) { return true; } @@ -226,9 +224,9 @@ export default defineComponent({ userInvitation(): boolean { if ( this.newUserRole && - this.newUserRole.projectId && + this.newUserRole.project?.id && this.newUserRole.user && - this.newUserRole.user.userId && + this.newUserRole.user.id && this.newUserRole.role && this.newUserRole.role.id ) { @@ -268,11 +266,9 @@ export default defineComponent({ this.$emit("setFilter", filter); }, selectUser(selectedUser: PublicUserDto | null) { - this.newUserRole.user = userMapper.map( - PublicUserDto2ProjectRoleUserDto, - selectedUser - ); - this.newUserRole.projectId = this.project?.id; + if (!selectedUser) return; + this.newUserRole.user = structuredClone(selectedUser); + this.newUserRole.project = { id: this.project?.id }; }, selectRole(roleId: string) { this.newUserRole.role = { diff --git a/src/modules/project/pages/components/modals/ImportUserModal.vue b/src/modules/project/pages/components/modals/ImportUserModal.vue index 84ddd19f7188f54eb5590ea67cc455055588c543..86d9e9959ccd2c63072905e101e6917f5ef6cb32 100644 --- a/src/modules/project/pages/components/modals/ImportUserModal.vue +++ b/src/modules/project/pages/components/modals/ImportUserModal.vue @@ -20,7 +20,7 @@ :key="additionalProject.id" :value="additionalProject.id" > - {{ additionalProject.projectName }} + {{ additionalProject.name }} </b-form-select-option> </b-form-select> </b-col> @@ -140,7 +140,7 @@ export default defineComponent({ (projectRole) => !this.projectRoles?.some( (currentProjectRole) => - projectRole.user?.userId === currentProjectRole.user?.userId + projectRole.user?.id === currentProjectRole.user?.id ) ); this.additionalMembers.forEach((additionalMember) => { @@ -157,7 +157,7 @@ export default defineComponent({ this.selectedProject = null; for (const projectRole of this.additionalMembers) { if (this.project) { - projectRole.projectId = this.project.id; + projectRole.project = { id: this.project.id }; } this.$emit("addUser", projectRole); } diff --git a/src/modules/project/store.ts b/src/modules/project/store.ts index 5a8885620c745e24b7f4bc49becde9bd09ce9058..127b1f594acb73e0b394d4db167e0da9eca844db 100644 --- a/src/modules/project/store.ts +++ b/src/modules/project/store.ts @@ -15,6 +15,7 @@ import { VisibilityApi, LicenseApi, ProjectQuotaApi, + SelfApi, } from "@coscine/api-client"; import type { Route } from "vue-router"; @@ -28,6 +29,7 @@ import type { ProjectForUpdateDto, ProjectInvitationDto, ProjectInvitationForProjectManipulationDto, + ProjectInvitationResolveDto, ProjectQuotaDto, ProjectQuotaForUpdateDto, ProjectRoleDto, @@ -125,7 +127,7 @@ export const useProjectStore = defineStore({ ) { const parentProjects = [] as ProjectDto[]; let currentParentProjectId = - this.visitedProjects[this.currentSlug].parentId; + this.visitedProjects[this.currentSlug].parent?.id; while ( currentParentProjectId && currentParentProjectId !== "00000000-0000-0000-0000-000000000000" @@ -138,7 +140,7 @@ export const useProjectStore = defineStore({ break; } parentProjects.push(parentProject); - currentParentProjectId = parentProject.parentId; + currentParentProjectId = parentProject.parent?.id; } else { break; } @@ -179,7 +181,7 @@ export const useProjectStore = defineStore({ if (currentRoles && currentUser) { const userProjectRole = currentRoles.find( (projectRole) => - projectRole.user && projectRole.user.userId === currentUser.id + projectRole.user && projectRole.user.id === currentUser.id ); return userProjectRole ? userProjectRole.role : null; } else { @@ -261,14 +263,22 @@ export const useProjectStore = defineStore({ async retrieveInvitations(project: ProjectDto | null) { const notificationStore = useNotificationStore(); try { - if (project && project.id && project.slug) { + if (project?.id && project?.slug) { const apiResponse = await ProjectInvitationApi.getProjectInvitations( project.id ); this.visitedProjects[project.slug].invitations = apiResponse.data.data ?? null; } else { - console.error("Selected project is null or its ID is undefined."); + if (!project) { + console.error("Selected project is null."); + } + if (project && !project.id) { + console.error("Selected project's ID is missing."); + } + if (project && !project.slug) { + console.error("Selected project's slug is missing."); + } } } catch (error) { // Handle other Status Codes @@ -284,7 +294,7 @@ export const useProjectStore = defineStore({ ): Promise<ResourceDtoIEnumerablePagedResponse> { const notificationStore = useNotificationStore(); try { - if (project && project.id && project.slug) { + if (project?.id && project?.slug) { const apiResponse = await ProjectResourceApi.getResourcesForProject( project.id, pageNumber, @@ -293,7 +303,15 @@ export const useProjectStore = defineStore({ ); return apiResponse.data; } else { - console.error("Selected project is null or its ID is undefined."); + if (!project) { + console.error("Selected project is null."); + } + if (project && !project.id) { + console.error("Selected project's ID is missing."); + } + if (project && !project.slug) { + console.error("Selected project's slug is missing."); + } } } catch (error) { // Handle other Status Codes @@ -352,14 +370,22 @@ export const useProjectStore = defineStore({ ): Promise<void> { const notificationStore = useNotificationStore(); try { - if (project && project.id && project.slug) { + if (project?.id && project?.slug) { const apiResponse = await ProjectQuotaApi.getProjectQuotas( project.id ); this.visitedProjects[project.slug].quotas = apiResponse.data.data ?? []; } else { - console.error("Selected project is null or its ID is undefined."); + if (!project) { + console.error("Selected project is null."); + } + if (project && !project.id) { + console.error("Selected project's ID is missing."); + } + if (project && !project.slug) { + console.error("Selected project's slug is missing."); + } } } catch (error) { // Handle other Status Codes @@ -438,7 +464,7 @@ export const useProjectStore = defineStore({ // Evaluate general resource type - RDS if (resource.type?.generalType === CoscineResourceTypes.Rds.General) { resourceTypeOptionsForManipulationDto.rdsResourceTypeOptions = { - size: desiredReservedQuota, + quota: desiredReservedQuota, }; } // Evaluate general resource type - RDS S3 @@ -446,7 +472,7 @@ export const useProjectStore = defineStore({ resource.type?.generalType === CoscineResourceTypes.RdsS3.General ) { resourceTypeOptionsForManipulationDto.rdsS3ResourceTypeOptions = { - size: desiredReservedQuota, + quota: desiredReservedQuota, }; } // Evaluate general resource type - RDS S3 WORM @@ -454,7 +480,7 @@ export const useProjectStore = defineStore({ resource.type?.generalType === CoscineResourceTypes.RdsS3Worm.General ) { resourceTypeOptionsForManipulationDto.rdsS3WormResourceTypeOptions = { - size: desiredReservedQuota, + quota: desiredReservedQuota, }; } // Assign the correct resource type options @@ -493,7 +519,7 @@ export const useProjectStore = defineStore({ const notificationStore = useNotificationStore(); try { const organizationDtos = await wrapListRequest((pageNumber: number) => - OrganizationApi.getOrganizations(filter, pageNumber, 50) + OrganizationApi.getOrganizations(filter, pageNumber, 150) ); this.organizations = organizationDtos; } catch (error) { @@ -532,7 +558,7 @@ export const useProjectStore = defineStore({ const notificationStore = useNotificationStore(); try { const disciplineDtos = await wrapListRequest((pageNumber: number) => - DisciplineApi.getDisciplines(pageNumber, 50, "displayNameDe asc") + DisciplineApi.getDisciplines(pageNumber, 150, "displayNameDe asc") ); this.disciplines = disciplineDtos ?? null; } catch (error) { @@ -554,18 +580,22 @@ export const useProjectStore = defineStore({ async resolveProjectInvitation() { const notificationStore = useNotificationStore(); + + // .../?invitationToken=<token> + const invitationTokenQueryParameter = "invitationToken"; + try { - // .../?invitationToken=<token> - const invitationToken = this.router.currentRoute.query.invitationToken; + const invitationToken = + this.router.currentRoute.query[invitationTokenQueryParameter]; if (invitationToken) { - //TODO: this is incorrect - await ProjectInvitationApi.resolveProjectInvitation( - invitationToken.toString() - ); + const projectInvitationResolveDto: ProjectInvitationResolveDto = { + token: invitationToken.toString(), + }; + await SelfApi.resolveProjectInvitation(projectInvitationResolveDto); this.refreshProjectInformation(); removeQueryParameterFromUrl( this.router.currentRoute, - "invitationToken" + invitationTokenQueryParameter ); } } catch (error) { @@ -573,7 +603,7 @@ export const useProjectStore = defineStore({ notificationStore.postApiErrorNotification(error as AxiosError); removeQueryParameterFromUrl( this.router.currentRoute, - "invitationToken" + invitationTokenQueryParameter ); } }, @@ -692,12 +722,20 @@ export const useProjectStore = defineStore({ async retrieveProjectRoles(project: ProjectDto | null) { const notificationStore = useNotificationStore(); try { - if (project && project.slug && project.id) { + if (project?.slug && project?.id) { const apiResponse = await ProjectMemberApi.getMemberships(project.id); this.visitedProjects[project.slug].roles = apiResponse.data.data ?? []; } else { - console.error("Selected project is null or its ID is undefined."); + if (!project) { + console.error("Selected project is null."); + } + if (project && !project.id) { + console.error("Selected project's ID is missing."); + } + if (project && !project.slug) { + console.error("Selected project's slug is missing."); + } } } catch (error) { // Handle other Status Codes @@ -708,14 +746,19 @@ export const useProjectStore = defineStore({ async deleteProjectRole(projectRole: ProjectRoleDto): Promise<boolean> { const notificationStore = useNotificationStore(); try { - if (projectRole.id && projectRole.projectId) { + if (projectRole.id && projectRole.project?.id) { await ProjectMemberApi.deleteMembership( - projectRole.projectId, + projectRole.project.id, projectRole.id ); return true; } else { - console.error("There was a problem with the project role."); + if (!projectRole.id) { + console.error("ProjectRole's ID is missing."); + } + if (!projectRole.project?.id) { + console.error("ProjectRole's project ID is missing."); + } return false; } } catch (error) { @@ -762,7 +805,7 @@ export const useProjectStore = defineStore({ async retrieveApplicationProfileAnalytics(project: ProjectDto | null) { const notificationStore = useNotificationStore(); try { - if (project && project.id) { + if (project?.id) { //await ProjectApi.projectCreateApplicationProfile(project.id); } else { console.error("Selected project is null or its ID is undefined."); @@ -776,16 +819,19 @@ export const useProjectStore = defineStore({ async deleteInvitation(projectInvitation: ProjectInvitationDto) { const notificationStore = useNotificationStore(); try { - if (projectInvitation.projectId && projectInvitation.id) { + if (projectInvitation.project?.id && projectInvitation.id) { await ProjectInvitationApi.deleteProjectInvitation( - projectInvitation.projectId, + projectInvitation.project.id, projectInvitation.id ); return true; } else { - console.error( - "Selected invitation id or projectId is null or undefined." - ); + if (!projectInvitation.project?.id) { + console.error("ProjectInvitation's projectId is missing."); + } + if (!projectInvitation.id) { + console.error("ProjectInvitation's ID is missing."); + } } } catch (error) { // Handle other Status Codes @@ -812,25 +858,28 @@ export const useProjectStore = defineStore({ async deleteProjectAssociation( project: ProjectDto, - projectRole: ProjectRoleDto | null + projectRole: ProjectRoleDto | null | undefined ) { const notificationStore = useNotificationStore(); try { - if ( - project.slug && - projectRole && - projectRole.projectId && - projectRole.id - ) { + if (project?.slug && projectRole?.project?.id && projectRole?.id) { await ProjectMemberApi.deleteMembership( - projectRole.projectId, + projectRole.project.id, projectRole.id ); this.visitedProjects[project.slug].roles = null; this.refreshProjectInformation(); this.router.push({ name: "home" }); } else { - console.error("Selected project is null or its ID is undefined."); + if (!project?.slug) { + console.error("Project slug is missing."); + } + if (!projectRole?.project?.id) { + console.error("ProjectRole's project ID is missing."); + } + if (!projectRole?.id) { + console.error("ProjectRole's ID is missing."); + } } } catch (error) { // Handle other Status Codes diff --git a/src/modules/resource/components/create-resource/ApplicationProfile.vue b/src/modules/resource/components/create-resource/ApplicationProfile.vue index afe80d1af34215a440d0a94291b27ae70cf39f53..271f729f9a72bedf87a32f995a3265b61cfcbd7f 100644 --- a/src/modules/resource/components/create-resource/ApplicationProfile.vue +++ b/src/modules/resource/components/create-resource/ApplicationProfile.vue @@ -229,7 +229,7 @@ export default defineComponent({ // An application profile has been selected. Set selection in dropdown. if (this.resourceForCreation.applicationProfile) { this.selectedApplicationProfile = this.applicationProfiles.find( - (ap) => ap.baseUri === this.resourceForCreation.applicationProfile + (ap) => ap.uri === this.resourceForCreation.applicationProfile.uri ); } }, @@ -268,7 +268,7 @@ export default defineComponent({ for (const applicationProfile of applicationProfiles) { // Extract AP hierarchy from URIs - const baseUri = applicationProfile.baseUri || ""; + const baseUri = applicationProfile.uri || ""; let text = baseUri.replace("https://purl.org/coscine/ap/", ""); if (text.charAt(text.length - 1) === "/") { text = text.substring(0, text.lastIndexOf("/")); @@ -310,7 +310,7 @@ export default defineComponent({ this.$set( this.resourceForCreation, "applicationProfile", - this.selectedApplicationProfile?.baseUri + this.selectedApplicationProfile?.uri ); this.v$.resourceForCreation.applicationProfile.$touch(); }, diff --git a/src/modules/resource/components/create-resource/Configuration.vue b/src/modules/resource/components/create-resource/Configuration.vue index bb451d09d7b70559692eb666970848bde4b8a9bf..ba23ee481795b8c10efacce7863205f0b1f09c8e 100644 --- a/src/modules/resource/components/create-resource/Configuration.vue +++ b/src/modules/resource/components/create-resource/Configuration.vue @@ -143,18 +143,18 @@ <!-- Popover --> <b-popover v-if=" - ((resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions?.size + ((resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions?.quota ?.value && - resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions?.size + resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions?.quota ?.value <= 0) || (resourceForCreation.resourceTypeOptions?.rdsS3ResourceTypeOptions - ?.size?.value && + ?.quota?.value && resourceForCreation.resourceTypeOptions?.rdsS3ResourceTypeOptions - ?.size?.value <= 0) || + ?.quota?.value <= 0) || (resourceForCreation.resourceTypeOptions?.rdsS3WormResourceTypeOptions - ?.size?.value && + ?.quota?.value && resourceForCreation.resourceTypeOptions - ?.rdsS3WormResourceTypeOptions?.size?.value <= 0)) && + ?.rdsS3WormResourceTypeOptions?.quota?.value <= 0)) && selectedResourceTypeInformation?.isQuotaAvailable " target="divButtonNext" @@ -265,8 +265,8 @@ export default defineComponent({ organizations(): string[] { if (this.userOrganizations) { return this.userOrganizations - .filter((organization) => !(organization.rorUri?.indexOf("#") !== -1)) // If does contain "#" it's a sub level organization, otherwise top level - .map((org) => (org.name ? org.name : "")) // Extract organization display name, could contain empty strings + .filter((organization) => !(organization.uri?.indexOf("#") !== -1)) // If does contain "#" it's a sub level organization, otherwise top level + .map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings .filter((n) => n); // Filter out empty strings, if any; } return []; diff --git a/src/modules/resource/components/create-resource/ConfigurationSizeSlider.vue b/src/modules/resource/components/create-resource/ConfigurationSizeSlider.vue index 2828efead43ee7580fdf3e26332ac02e162f76b2..6e04f7dba7dfe3c55be89cef33c7492afd4a654d 100644 --- a/src/modules/resource/components/create-resource/ConfigurationSizeSlider.vue +++ b/src/modules/resource/components/create-resource/ConfigurationSizeSlider.vue @@ -134,7 +134,7 @@ export default defineComponent({ if (this.projectStore.currentResourceTypesQuotas) { const resourceTypeId = this.resourceForCreation.resourceTypeId; const quota = this.projectStore.currentResourceTypesQuotas.find( - (q) => q.resourceTypeId === resourceTypeId + (q) => q.resourceType?.id === resourceTypeId ); if (quota) { // Calculate slider maximum from Reserved and Used @@ -171,26 +171,26 @@ export default defineComponent({ // Handle general resource type - RDS if ( this.resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions - ?.size?.value !== undefined + ?.quota?.value !== undefined ) { this.sliderValue = - this.resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions?.size?.value; + this.resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions?.quota?.value; } // Handle general resource type - RDS S3 else if ( this.resourceForCreation.resourceTypeOptions?.rdsS3ResourceTypeOptions - ?.size?.value !== undefined + ?.quota?.value !== undefined ) { this.sliderValue = - this.resourceForCreation.resourceTypeOptions?.rdsS3ResourceTypeOptions?.size?.value; + this.resourceForCreation.resourceTypeOptions?.rdsS3ResourceTypeOptions?.quota?.value; } // Handle general resource type - RDS S3 WORM else if ( this.resourceForCreation.resourceTypeOptions - ?.rdsS3WormResourceTypeOptions?.size?.value !== undefined + ?.rdsS3WormResourceTypeOptions?.quota?.value !== undefined ) { this.sliderValue = - this.resourceForCreation.resourceTypeOptions?.rdsS3WormResourceTypeOptions?.size?.value; + this.resourceForCreation.resourceTypeOptions?.rdsS3WormResourceTypeOptions?.quota?.value; } } }, diff --git a/src/modules/resource/components/create-resource/Overview.vue b/src/modules/resource/components/create-resource/Overview.vue index ad7c461901dd18cc21236ea441cfd95791b62f5d..585a6a20ad9f9179be40e6ba96f5cfba0f49a222 100644 --- a/src/modules/resource/components/create-resource/Overview.vue +++ b/src/modules/resource/components/create-resource/Overview.vue @@ -175,32 +175,32 @@ export default defineComponent({ resourceSizeText(): string { if ( this.resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions - ?.size + ?.quota ) { return this.$t("default.gb", { number: this.toGiB( this.resourceForCreation.resourceTypeOptions?.rdsResourceTypeOptions - ?.size + ?.quota ), }).toString(); } else if ( this.resourceForCreation.resourceTypeOptions?.rdsS3ResourceTypeOptions - ?.size + ?.quota ) { return this.$t("default.gb", { number: this.toGiB( this.resourceForCreation.resourceTypeOptions - ?.rdsS3ResourceTypeOptions?.size + ?.rdsS3ResourceTypeOptions?.quota ), }).toString(); } else if ( this.resourceForCreation.resourceTypeOptions - ?.rdsS3WormResourceTypeOptions?.size + ?.rdsS3WormResourceTypeOptions?.quota ) { return this.$t("default.gb", { number: this.toGiB( this.resourceForCreation.resourceTypeOptions - ?.rdsS3WormResourceTypeOptions?.size + ?.rdsS3WormResourceTypeOptions?.quota ), }).toString(); } else { diff --git a/src/modules/resource/components/create-resource/ResourceMetadata.vue b/src/modules/resource/components/create-resource/ResourceMetadata.vue index 25cebb69bf657dd6c7174385f7b03ceb14abde04..c9920a098e2c55763aaa59ef8a376c349cb4cee6 100644 --- a/src/modules/resource/components/create-resource/ResourceMetadata.vue +++ b/src/modules/resource/components/create-resource/ResourceMetadata.vue @@ -12,14 +12,14 @@ > <b-form-input id="ResourceName" - v-model="v$.resourceForUpdate.resourceName.$model" + v-model="v$.resourceForUpdate.name.$model" :state=" - v$.resourceForUpdate.resourceName.$dirty - ? !v$.resourceForUpdate.resourceName.$error + v$.resourceForUpdate.name.$dirty + ? !v$.resourceForUpdate.name.$error : null " :placeholder="$t('form.resource.resourceName')" - :maxlength="v$.resourceForUpdate.resourceName.maxLength.$params.max" + :maxlength="v$.resourceForUpdate.name.maxLength.$params.max" required :readonly="readonly" @input="translateResourceNameToDisplayName" @@ -27,8 +27,7 @@ <div class="invalid-tooltip"> {{ $t("form.resource.resourceNameHelp", { - maxLength: - v$.resourceForUpdate.resourceName.maxLength.$params.max, + maxLength: v$.resourceForUpdate.name.maxLength.$params.max, }) }} </div> @@ -329,7 +328,7 @@ export default defineComponent({ }); const rules = { resourceForUpdate: { - resourceName: { required, maxLength: maxLength(200) }, + name: { required, maxLength: maxLength(200) }, displayName: { required, maxLength: maxLength(25) }, description: { required, maxLength: maxLength(5000) }, disciplines: { required }, @@ -532,9 +531,9 @@ export default defineComponent({ * Translate the resource name into display name if not locked and assign the first 25 characters */ translateResourceNameToDisplayName() { - if (!this.isLockedDisplayName && this.resourceForUpdate.resourceName) { + if (!this.isLockedDisplayName && this.resourceForUpdate.name) { this.resourceForUpdate.displayName = - this.resourceForUpdate.resourceName.substring(0, 25); + this.resourceForUpdate.name.substring(0, 25); this.v$.resourceForUpdate.displayName?.$touch(); } }, diff --git a/src/modules/resource/components/resource-page/FilesView.spec.ts b/src/modules/resource/components/resource-page/FilesView.spec.ts index 66a621a117744d64d173583c11c5b0520973c7f2..6f58697721fea20a9d9b2b85c6042a9932aa7d3a 100644 --- a/src/modules/resource/components/resource-page/FilesView.spec.ts +++ b/src/modules/resource/components/resource-page/FilesView.spec.ts @@ -20,7 +20,7 @@ import { routes } from "@/router"; import type Vue from "vue"; -import { getTestUserState } from "@/data/mockup/testUser"; +import { getTestShibbolethUserState } from "@/data/mockup/testUser"; import { getTestResourceState } from "@/data/mockup/testResource"; import { testProjectState } from "@/data/mockup/testProject"; import useResourceStore from "../../store"; @@ -53,7 +53,7 @@ describe("FilesView.vue", async () => { initialState: { project: testProjectState, resource: await getTestResourceState(), - user: getTestUserState(), + user: getTestShibbolethUserState(), }, }); diff --git a/src/modules/resource/components/resource-page/FilesView.vue b/src/modules/resource/components/resource-page/FilesView.vue index ace291b8ef67e76d3df33bfcf28397071d7041ae..da0c9b795c597d12b3efffb60776936be6c6077e 100644 --- a/src/modules/resource/components/resource-page/FilesView.vue +++ b/src/modules/resource/components/resource-page/FilesView.vue @@ -185,8 +185,8 @@ import type { TreeDataType, ProjectDto, ResourceTypeInformationDto, - FileDto, - MetadataDto, + FileTreeDto, + MetadataTreeDto, } from "@coscine/api-client/dist/types/Coscine.Api"; import type { BilingualLabels, @@ -776,13 +776,13 @@ export default defineComponent({ /** * Constructs the folder content based on the provided file and metadata trees. * @async - * @param {FileDto[] | null | undefined} fileTree - File tree of the folder. - * @param {MetadataDto[] | null | undefined} metadataTree - Metadata tree of the folder. + * @param {FileTreeDto[] | null | undefined} fileTree - File tree of the folder. + * @param {MetadataTreeDto[] | null | undefined} metadataTree - Metadata tree of the folder. * @returns {FolderContent[]} An array containing folder content. */ async constructFolderContents( - fileTree: FileDto[] | null | undefined, - metadataTree: MetadataDto[] | null | undefined + fileTree: FileTreeDto[] | null | undefined, + metadataTree: MetadataTreeDto[] | null | undefined ): Promise<FolderContent[]> { const folderContents = []; @@ -798,10 +798,13 @@ export default defineComponent({ const metadataEntry = metadataTree?.find( (md) => md.path === fileEntry.path ); - if (metadataEntry?.definition && metadataEntry?.format) { + if ( + metadataEntry?.definition?.type && + metadataEntry?.definition?.content + ) { content.metadata = await parseRDFDefinition( - metadataEntry.definition, - metadataEntry.format + metadataEntry.definition.content, + metadataEntry.definition.type ); } folderContents.push(content); @@ -832,10 +835,10 @@ export default defineComponent({ /** * Maps a file entry to its corresponding folder content. - * @param {FileDto} fileEntry - The file data transfer object to map. + * @param {FileTreeDto} fileEntry - The file data transfer object to map. * @returns {FolderContent | undefined} The corresponding folder content or undefined if the type is unknown. */ - mapFileEntryToContent(fileEntry: FileDto): FolderContent | undefined { + mapFileEntryToContent(fileEntry: FileTreeDto): FolderContent | undefined { let content: FolderContent | undefined = undefined; switch (fileEntry.type) { case "Tree" as TreeDataType.Tree: diff --git a/src/modules/resource/components/resource-page/FilesViewHeader.vue b/src/modules/resource/components/resource-page/FilesViewHeader.vue index 6c2e4079924d7701fc041ea4d691efaabd3604c7..42dd8e9cfe9c0f47c0d4e4e3219dade3d07a20e5 100644 --- a/src/modules/resource/components/resource-page/FilesViewHeader.vue +++ b/src/modules/resource/components/resource-page/FilesViewHeader.vue @@ -289,18 +289,17 @@ export default defineComponent({ methods: { async getGitlabProjectName() { if ( - this.resource?.type?.options?.gitLabOptions?.repoUrl && - this.resource?.type?.options?.gitLabOptions?.accessToken + this.resource?.type?.options?.gitLab?.repoUrl && + this.resource?.type?.options?.gitLab?.accessToken ) { this.gitlabProjects = await this.resourceStore.getGitlabAllProjects( - this.resource?.type?.options?.gitLabOptions?.repoUrl, - this.resource?.type?.options?.gitLabOptions?.accessToken + this.resource?.type?.options?.gitLab?.repoUrl, + this.resource?.type?.options?.gitLab?.accessToken ); if (this.gitlabProjects) { this.gitlabInformation.project = this.gitlabProjects.find( - (p) => - p.id === this.resource?.type?.options?.gitLabOptions?.projectId + (p) => p.id === this.resource?.type?.options?.gitLab?.projectId ) ?? null; } } diff --git a/src/modules/resource/components/resource-page/MetadataManager.vue b/src/modules/resource/components/resource-page/MetadataManager.vue index 7e8a614c9b389df6890a7f4ce23ee21066cd566c..48a335a8c68d4c47de18ead3e5004d53989df38a 100644 --- a/src/modules/resource/components/resource-page/MetadataManager.vue +++ b/src/modules/resource/components/resource-page/MetadataManager.vue @@ -61,7 +61,9 @@ <!-- Form Generator --> <span v-if=" - resource && resource.applicationProfile !== '' && applicationProfile + resource && + resource?.applicationProfile?.uri !== '' && + applicationProfile " class="generatedFormSpan" > @@ -71,7 +73,7 @@ :fixed-values="resource.fixedValues" :form-data="currentUsedMetadata" :locale="$root.$i18n.locale" - :selected-shape="resource.applicationProfile" + :selected-shape="resource.applicationProfile?.uri" :shapes="applicationProfile" :disabled-mode="isGuest || resource.archived" :validation-context="currentFileId + ''" @@ -665,8 +667,10 @@ export default defineComponent({ // Create the metadata tree DTO and serialize the definition const metadataTreeForCreationDto: MetadataTreeForCreationDto = { path: file.path, - definition: await serializeRDFDefinition(file.metadata, contentType), - format: contentType as RdfFormat, + definition: { + content: await serializeRDFDefinition(file.metadata, contentType), + type: contentType as RdfFormat, + }, }; // Trigger metadata tree creation. Metadata might be already present, so we try updating it if true. @@ -850,8 +854,10 @@ export default defineComponent({ // Create the metadata tree DTO and serialize the definition const metadataTreeForUpdateDto: MetadataTreeForUpdateDto = { path: file.path, - definition: await serializeRDFDefinition(file.metadata, contentType), - format: contentType as RdfFormat, + definition: { + content: await serializeRDFDefinition(file.metadata, contentType), + type: contentType as RdfFormat, + }, }; const success = await this.resourceStore.updateOrAddMetadataTree( diff --git a/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue b/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue index f3923c7d8ef1885bace2c50b6fc70110b124818d..6a484e3a08d3110fd7c6d79e5df76e51d6dad340 100644 --- a/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue +++ b/src/modules/resource/components/resource-page/metadata/MetadataManagerHeader.vue @@ -157,17 +157,16 @@ export default defineComponent({ }, }, async mounted() { - if (this.resource.type?.options?.gitLabOptions && this.isGitLab) { + if (this.resource.type?.options?.gitLab && this.isGitLab) { await this.fetchGitlabBranchInfo(); } }, methods: { async fetchGitlabBranchInfo() { - const projectId = this.resource.type?.options?.gitLabOptions?.projectId; - const domain = this.resource.type?.options?.gitLabOptions?.repoUrl; - const accessToken = - this.resource.type?.options?.gitLabOptions?.accessToken; - const currentBranch = this.resource.type?.options?.gitLabOptions?.branch; + const projectId = this.resource.type?.options?.gitLab?.projectId; + const domain = this.resource.type?.options?.gitLab?.repoUrl; + const accessToken = this.resource.type?.options?.gitLab?.accessToken; + const currentBranch = this.resource.type?.options?.gitLab?.branch; if (projectId && domain && accessToken && currentBranch) { const gitlabBranches = await this.resourceStore.getGitlabBranchesForProject( diff --git a/src/modules/resource/components/resource-page/resource-type/GitLab.vue b/src/modules/resource/components/resource-page/resource-type/GitLab.vue index 17e9c61e553f1a6561bb2a7af2360956ad1467d2..1b273ea3e4aeabcd24a080016dd201805c02555f 100644 --- a/src/modules/resource/components/resource-page/resource-type/GitLab.vue +++ b/src/modules/resource/components/resource-page/resource-type/GitLab.vue @@ -1,16 +1,16 @@ <template> <div> - <li v-if="resource.type?.options?.gitLabOptions?.repoUrl"> + <li v-if="resource.type?.options?.gitLab?.repoUrl"> <b>{{ `${$t("resourceType.gitlab.domainLabel")} ` }}</b> - {{ resource.type.options.gitLabOptions.repoUrl }} + {{ resource.type.options.gitLab.repoUrl }} </li> - <li v-if="resource.type?.options?.gitLabOptions?.projectId"> + <li v-if="resource.type?.options?.gitLab?.projectId"> <b>{{ `${$t("resourceType.gitlab.projectLabel")} ` }}</b> {{ gitlabInformation.project ? gitlabInformation.project.name : "" }} </li> - <li v-if="resource.type?.options?.gitLabOptions?.branch"> + <li v-if="resource.type?.options?.gitLab?.branch"> <b>{{ `${$t("resourceType.gitlab.referenceLabel")} ` }}</b> - {{ resource.type.options.gitLabOptions.branch }} + {{ resource.type.options.gitLab.branch }} </li> </div> </template> @@ -57,18 +57,17 @@ export default defineComponent({ methods: { async getGitlabProjectName() { if ( - this.resource?.type?.options?.gitLabOptions?.repoUrl && - this.resource?.type?.options?.gitLabOptions?.accessToken + this.resource?.type?.options?.gitLab?.repoUrl && + this.resource?.type?.options?.gitLab?.accessToken ) { this.gitlabProjects = await this.resourceStore.getGitlabAllProjects( - this.resource?.type?.options?.gitLabOptions?.repoUrl, - this.resource?.type?.options?.gitLabOptions?.accessToken + this.resource?.type?.options?.gitLab?.repoUrl, + this.resource?.type?.options?.gitLab?.accessToken ); if (this.gitlabProjects) { this.gitlabInformation.project = this.gitlabProjects.find( - (p) => - p.id === this.resource?.type?.options?.gitLabOptions?.projectId + (p) => p.id === this.resource?.type?.options?.gitLab?.projectId ) ?? null; } } diff --git a/src/modules/resource/components/settings/Actions.vue b/src/modules/resource/components/settings/Actions.vue index 90eee0c02c92c072a8050845f37d5f1c602a840a..632318cecfe484f8e155ab868446abbc5020c686 100644 --- a/src/modules/resource/components/settings/Actions.vue +++ b/src/modules/resource/components/settings/Actions.vue @@ -5,7 +5,6 @@ label-for="Archive" :label="$t('page.settings.actions.resourceArchiveLabel')" :is-loading="isLoading" - class="d-flex align-items-center" info > <template #popover> @@ -34,7 +33,6 @@ label-for="DeleteResource" :label="$t('page.settings.actions.resourceDeleteLabel')" :is-loading="isLoading" - class="d-flex align-items-center" > <!-- Delete Button --> <b-button diff --git a/src/modules/resource/components/settings/ApplicationProfile.vue b/src/modules/resource/components/settings/ApplicationProfile.vue index 59386ad5c95ffe97fb9132cc4020e1b1516bd69d..b657bf56b580d0c0aff821c007f88e5cb49a4b65 100644 --- a/src/modules/resource/components/settings/ApplicationProfile.vue +++ b/src/modules/resource/components/settings/ApplicationProfile.vue @@ -6,12 +6,12 @@ </b-row> <FormGenerator v-else-if="resource" - :key="resource.applicationProfile" + :key="resource.applicationProfile?.uri" :disabled-mode="!isUserAllowedToEdit || resource.archived" :fixed-value-mode="true" :fixed-values="resourceForm.fixedValues" :locale="$root.$i18n.locale" - :selected-shape="resource.applicationProfile" + :selected-shape="resource.applicationProfile?.uri" :shapes="applicationProfile" :class-receiver="getVocabularyInstances" :user-receiver="async () => user" diff --git a/src/modules/resource/components/settings/Overview.vue b/src/modules/resource/components/settings/Overview.vue index a2decb2ecbfe2ded12a0f5e355b1ecabe60bf043..548d5a9ab695a08eac469aa08b7af99c2799b31a 100644 --- a/src/modules/resource/components/settings/Overview.vue +++ b/src/modules/resource/components/settings/Overview.vue @@ -58,7 +58,7 @@ <b-form-input v-if="resource" id="ResourceNameReadOnly" - v-model="resource.resourceName" + v-model="resource.name" required :readonly="readonly" /> diff --git a/src/modules/resource/components/settings/resource-type/Generic.vue b/src/modules/resource/components/settings/resource-type/Generic.vue index 16231a9623879cc42a78c78904ac8a818fbaa594..90ae1a781696e71eaf2f3c7f0da5ebfde83231ad 100644 --- a/src/modules/resource/components/settings/resource-type/Generic.vue +++ b/src/modules/resource/components/settings/resource-type/Generic.vue @@ -108,27 +108,27 @@ export default defineComponent({ if ( this.resource?.type?.generalType === this.coscineResourceTypes.Rds.General && - this.resource?.type?.options?.rdsOptions + this.resource?.type?.options?.rds ) { - return this.resource?.type?.options?.rdsOptions; + return this.resource?.type?.options?.rds; } else if ( this.resource?.type?.generalType === this.coscineResourceTypes.RdsS3.General && - this.resource?.type?.options?.rdsS3Options + this.resource?.type?.options?.rdsS3 ) { - return this.resource?.type?.options?.rdsS3Options; + return this.resource?.type?.options?.rdsS3; } else if ( this.resource?.type?.generalType === this.coscineResourceTypes.RdsS3Worm.General && - this.resource?.type?.options?.rdsS3WormOptions + this.resource?.type?.options?.rdsS3Worm ) { - return this.resource?.type?.options?.rdsS3WormOptions; + return this.resource?.type?.options?.rdsS3Worm; } else if ( this.resource?.type?.generalType === this.coscineResourceTypes.LinkedData.General && - this.resource?.type?.options?.linkedDataOptions + this.resource?.type?.options?.linkedData ) { - return this.resource?.type?.options?.linkedDataOptions; + return this.resource?.type?.options?.linkedData; } else return {}; }, }, diff --git a/src/modules/resource/components/settings/resource-type/GitLab.vue b/src/modules/resource/components/settings/resource-type/GitLab.vue index dfcd4fcb81aca57fde432d7298b47a8530d64ff3..b150e922e496f07c9ddf3aef2a5b78ba404eadd0 100644 --- a/src/modules/resource/components/settings/resource-type/GitLab.vue +++ b/src/modules/resource/components/settings/resource-type/GitLab.vue @@ -10,7 +10,7 @@ > <b-form-input id="Domain" - :value="resource?.type?.options?.gitLabOptions?.repoUrl" + :value="resource?.type?.options?.gitLab?.repoUrl" :placeholder="$t('resourceType.gitlab.domain')" readonly /> @@ -26,7 +26,7 @@ > <b-form-input id="ProjectId" - :value="resource?.type?.options?.gitLabOptions?.projectId" + :value="resource?.type?.options?.gitLab?.projectId" :placeholder="$t('resourceType.gitlab.projectId')" readonly /> @@ -276,8 +276,7 @@ export default defineComponent({ this.gitlabInformation.project = this.gitlabProjects.find( - (p) => - p.id === this.resource?.type?.options?.gitLabOptions?.projectId + (p) => p.id === this.resource?.type?.options?.gitLab?.projectId ) ?? null; await this.setSelectedGitLabProject(); if (this.gitlabBranches) { @@ -365,12 +364,12 @@ export default defineComponent({ async initTabContent() { this.isLoading = true; if ( - this.resource?.type?.options?.gitLabOptions?.repoUrl && + this.resource?.type?.options?.gitLab?.repoUrl && this.resourceForUpdate.resourceTypeOptions?.gitlabResourceTypeOptions ) { // A resource type has been selected, but the resource's properties are unset. Set the values. this.gitlabInformation.domain = - this.resource.type.options?.gitLabOptions.repoUrl; + this.resource.type.options?.gitLab.repoUrl; this.gitlabInformation.accessToken = this.resourceForUpdate.resourceTypeOptions.gitlabResourceTypeOptions.accessToken; await this.verifyDomainAndToken(true); diff --git a/src/modules/resource/pages/CreateResource.vue b/src/modules/resource/pages/CreateResource.vue index a09ca977129a00731b0c7719712646dbaec02bb9..652da67569d062fab519027fd22261b4cf59ddc0 100644 --- a/src/modules/resource/pages/CreateResource.vue +++ b/src/modules/resource/pages/CreateResource.vue @@ -112,12 +112,12 @@ export default defineComponent({ description: "", displayName: "", fixedValues: {}, - resourceName: "", + name: "", keywords: [] as string[], usageRights: "", disciplines: [] as DisciplineForResourceManipulationDto[], visibility: {} as VisibilityForResourceManipulationDto, - applicationProfile: "", + applicationProfile: { uri: "" }, resourceTypeId: "", resourceTypeOptions: {} as ResourceTypeOptionsForCreationDto, } as ResourceForCreationDto, @@ -225,7 +225,7 @@ export default defineComponent({ this.isLoadingFormGenerator = true; const applicationProfile = await this.resourceStore.getApplicationProfile( - this.resourceForCreation.applicationProfile + this.resourceForCreation.applicationProfile.uri ); this.applicationProfileDefinition = applicationProfile; this.isLoadingFormGenerator = false; diff --git a/src/modules/resource/pages/ResourcePage.spec.ts b/src/modules/resource/pages/ResourcePage.spec.ts index 8b4ceb1a4693bf23f6e8b7cddd938c2a95a075c1..5a218ccc6030a903db7c524770fd4e2182929386 100644 --- a/src/modules/resource/pages/ResourcePage.spec.ts +++ b/src/modules/resource/pages/ResourcePage.spec.ts @@ -20,7 +20,7 @@ import { routes } from "@/router"; import Vue from "vue"; -import { getTestUserState } from "@/data/mockup/testUser"; +import { getTestShibbolethUserState } from "@/data/mockup/testUser"; import { getTestResourceState } from "@/data/mockup/testResource"; import { testProjectState } from "@/data/mockup/testProject"; import useResourceStore from "../store"; @@ -53,7 +53,7 @@ describe("ResourcePage.vue", async () => { initialState: { project: testProjectState, resource: await getTestResourceState(), - user: getTestUserState(), + user: getTestShibbolethUserState(), }, }); diff --git a/src/modules/resource/pages/Settings.vue b/src/modules/resource/pages/Settings.vue index 07a843bfc430aa416d6309d6659ca110beaf6bba..25baff464be52a7a333a98f6e3bb57ee0b028ff1 100644 --- a/src/modules/resource/pages/Settings.vue +++ b/src/modules/resource/pages/Settings.vue @@ -134,7 +134,7 @@ export default defineComponent({ resourceForUpdate: { description: "", displayName: "", - resourceName: "", + name: "", keywords: [] as string[], license: {} as LicenseForResourceManipulationDto, usageRights: "", @@ -168,15 +168,15 @@ export default defineComponent({ | undefined { switch (this.resource?.type?.generalType) { case this.coscineResourceTypes.LinkedData.General: - return this.resource.type.options?.linkedDataOptions; + return this.resource.type.options?.linkedData; case this.coscineResourceTypes.Gitlab.General: - return this.resource.type.options?.gitLabOptions; + return this.resource.type.options?.gitLab; case this.coscineResourceTypes.Rds.General: - return this.resource.type.options?.rdsOptions; + return this.resource.type.options?.rds; case this.coscineResourceTypes.RdsS3.General: - return this.resource.type.options?.rdsS3Options; + return this.resource.type.options?.rdsS3; case this.coscineResourceTypes.RdsS3Worm.General: - return this.resource.type.options?.rdsS3WormOptions; + return this.resource.type.options?.rdsS3Worm; default: return undefined; } @@ -331,7 +331,7 @@ export default defineComponent({ this.isLoading = true; const applicationProfile = await this.resourceStore.getApplicationProfile( - this.resource.applicationProfile + this.resource.applicationProfile.uri ?? "" ); this.applicationProfile = applicationProfile; this.isLoading = false; diff --git a/src/modules/resource/store.ts b/src/modules/resource/store.ts index 2e8398d8ad83708d7b557186f5ec20187df13542..049a1b3a7f55d603c5496dc0502f91a29228e1e2 100644 --- a/src/modules/resource/store.ts +++ b/src/modules/resource/store.ts @@ -37,8 +37,8 @@ import type { ResourceForUpdateDto, ResourceQuotaDto, AcceptedLanguage, - FileDto, - MetadataDto, + FileTreeDto, + MetadataTreeDto, } from "@coscine/api-client/dist/types/Coscine.Api/api"; import { wrapListRequest } from "@/util/wrapListRequest"; /* @@ -153,24 +153,24 @@ export const useResourceStore = defineStore({ async retrieveApplicationProfile(resource: VisitedResourceObject) { const notificationStore = useNotificationStore(); try { - if (resource.applicationProfile) { + if (resource.applicationProfile?.uri) { const apiResponse = await ApplicationProfileApi.getApplicationProfile( - resource.applicationProfile, + resource.applicationProfile.uri, "JsonLd" as RdfFormat ); const returnedData = apiResponse.data.data; if ( - returnedData?.definition && - returnedData?.format && - returnedData?.baseUri + returnedData?.definition?.content && + returnedData?.definition?.type && + returnedData?.uri ) { resource.rawApplicationProfile = await parseRDFDefinition( - returnedData.definition, - returnedData.format, - returnedData.baseUri + returnedData.definition.content, + returnedData.definition.type, + returnedData.uri ); resource.fullApplicationProfile = await resolveImports( - returnedData.baseUri, + returnedData.uri, resource.rawApplicationProfile ); } else { @@ -197,18 +197,18 @@ export const useResourceStore = defineStore({ ); const returnedData = apiResponse.data.data; if ( - returnedData?.definition && - returnedData?.format && - returnedData?.baseUri + returnedData?.definition?.content && + returnedData?.definition?.type && + returnedData?.uri ) { let returnApplicationProfile = await parseRDFDefinition( - returnedData.definition, - returnedData.format, - returnedData.baseUri + returnedData.definition.content, + returnedData.definition.type, + returnedData.uri ); if (doResolveImports) { returnApplicationProfile = await resolveImports( - returnedData.baseUri, + returnedData.uri, returnApplicationProfile ); } @@ -608,7 +608,7 @@ export const useResourceStore = defineStore({ resourceId: string, filePath: string, format?: RdfFormat - ): Promise<MetadataDto[] | null | undefined> { + ): Promise<MetadataTreeDto[] | null | undefined> { const notificationStore = useNotificationStore(); try { const response = await TreeApi.getMetadataTree( @@ -629,7 +629,7 @@ export const useResourceStore = defineStore({ projectId: string, resourceId: string, filePath: string - ): Promise<FileDto[] | null | undefined> { + ): Promise<FileTreeDto[] | null | undefined> { const notificationStore = useNotificationStore(); try { const response = await TreeApi.getFileTree( diff --git a/src/modules/resource/utils/MetadataManagerUtil.ts b/src/modules/resource/utils/MetadataManagerUtil.ts index c49ef45e1c2a5c8eb2f4cc6345a1e6b067c94176..21b403a89e4bd19ecd4b5a19bfdae9658e12a1cf 100644 --- a/src/modules/resource/utils/MetadataManagerUtil.ts +++ b/src/modules/resource/utils/MetadataManagerUtil.ts @@ -40,10 +40,13 @@ export default { ); // If the metadata exists and has a definition, parse it - if (metadataTree?.[0]?.definition && metadataTree[0].format) { + if ( + metadataTree?.[0]?.definition?.content && + metadataTree[0].definition.type + ) { return await parseRDFDefinition( - metadataTree[0].definition, - metadataTree[0].format + metadataTree[0].definition.content, + metadataTree[0].definition.type ); } diff --git a/src/modules/resource/utils/linkedData.ts b/src/modules/resource/utils/linkedData.ts index c0632b8abfb61ecb0871340f3e31aa496472d81b..76d08fc98ffd286e8c94c684947932486d8b2a3a 100644 --- a/src/modules/resource/utils/linkedData.ts +++ b/src/modules/resource/utils/linkedData.ts @@ -87,10 +87,13 @@ export async function resolveImports( "JsonLd" as RdfFormat ); const apResponse = importedApiResponse.data.data; - if (apResponse?.definition && apResponse?.format) { + if ( + apResponse?.definition?.content && + apResponse?.definition.type + ) { const importedApplicationProfile = await parseRDFDefinition( - apResponse.definition, - apResponse.format, + apResponse.definition.content, + apResponse.definition.type, importedAP.value ); fullApplicationProfile = ( diff --git a/src/modules/resource/utils/validators.ts b/src/modules/resource/utils/validators.ts index ab58e281ae0893f62d84cd52b1fbd0ed5f4eb815..0ba1ba78e9c03741bcbfd134322391bab4b5161b 100644 --- a/src/modules/resource/utils/validators.ts +++ b/src/modules/resource/utils/validators.ts @@ -80,8 +80,8 @@ const hasValidResourceTypeOptionsWithQuota = ( | RdsS3ResourceTypeOptionsForManipulationDto | RdsS3WormResourceTypeOptionsForManipulationDto ): boolean => { - if (options !== undefined && options.size) { - const { value, unit } = options.size; + if (options !== undefined && options.quota) { + const { value, unit } = options.quota; const isValidUnit = Object.values(QuotaUnit).includes(unit as QuotaUnit); const isValidValue = Number.isFinite(value) && value > 0; return isValidUnit && isValidValue; diff --git a/src/modules/user/UserModule.vue b/src/modules/user/UserModule.vue index 1d1a3add9b9a2d5d7053005cf20fe40ac423bedc..3eae27543ef74638810f448468aecb2691890658 100644 --- a/src/modules/user/UserModule.vue +++ b/src/modules/user/UserModule.vue @@ -52,5 +52,3 @@ export default defineComponent({ }, }); </script> - -<style></style> diff --git a/src/modules/user/i18n/de.ts b/src/modules/user/i18n/de.ts index 5eecc836a628f786809e5b5ef480be65c1069c34..c009f0c91efd914790d54c34186c58753202d4e1 100644 --- a/src/modules/user/i18n/de.ts +++ b/src/modules/user/i18n/de.ts @@ -15,8 +15,8 @@ export default { header: "Persönliche Daten", title: "Titel", - firstName: "Vorname", - lastName: "Name", + givenName: "Vorname", + familyName: "Nachname", email: "E-Mail", organization: "Organisation", institute: "Institut", @@ -41,15 +41,17 @@ export default { "Keine Ergebnisse verfügbar. Bitte passen Sie die Suchanfrage an.", noOptions: "Die Liste ist leer.", noOptionsOrganization: "Suche nach Organisationen", + searchNotEnoughCharacters: + "Bitte geben Sie mindestens {min} Zeichen ein, um zu suchen.", }, labels: { titleLabel: "@:(page.userprofile.form.personalInformation.title)@:(page.userprofile.form.labelSymbol)", firstNameLabel: - "@:(page.userprofile.form.personalInformation.firstName)@:(page.userprofile.form.labelSymbol)", + "@:(page.userprofile.form.personalInformation.givenName)@:(page.userprofile.form.labelSymbol)", lastNameLabel: - "@:(page.userprofile.form.personalInformation.lastName)@:(page.userprofile.form.labelSymbol)", + "@:(page.userprofile.form.personalInformation.familyName)@:(page.userprofile.form.labelSymbol)", emailLabel: "@:(page.userprofile.form.personalInformation.email)@:(page.userprofile.form.labelSymbol)", organizationLabel: diff --git a/src/modules/user/i18n/en.ts b/src/modules/user/i18n/en.ts index b36d6be9b678ec66b7145c2ada202932173508da..8ae1524aee7c4dd656995fbbe2c0faa4cefe889a 100644 --- a/src/modules/user/i18n/en.ts +++ b/src/modules/user/i18n/en.ts @@ -15,8 +15,8 @@ export default { header: "Personal Information", title: "Title", - firstName: "First Name", - lastName: "Last Name", + givenName: "Given Name", + familyName: "Family Name", email: "Email", organization: "Organization", institute: "Institute", @@ -39,15 +39,17 @@ export default { noResults: "No elements found. Consider changing the search query.", noOptions: "List is empty.", noOptionsOrganization: "Search for organizations", + searchNotEnoughCharacters: + "Please enter at least {min} characters to search.", }, labels: { titleLabel: "@:(page.userprofile.form.personalInformation.title)@:(page.userprofile.form.labelSymbol)", firstNameLabel: - "@:(page.userprofile.form.personalInformation.firstName)@:(page.userprofile.form.labelSymbol)", + "@:(page.userprofile.form.personalInformation.givenName)@:(page.userprofile.form.labelSymbol)", lastNameLabel: - "@:(page.userprofile.form.personalInformation.lastName)@:(page.userprofile.form.labelSymbol)", + "@:(page.userprofile.form.personalInformation.familyName)@:(page.userprofile.form.labelSymbol)", emailLabel: "@:(page.userprofile.form.personalInformation.email)@:(page.userprofile.form.labelSymbol)", organizationLabel: diff --git a/src/modules/user/pages/UserProfile.spec.ts b/src/modules/user/pages/UserProfile.spec.ts index b40611347ed6b90485fbca07a2f7fd62a99fafeb..e8b54c116e341e3d79142ac6eaff2ae41a712b7f 100644 --- a/src/modules/user/pages/UserProfile.spec.ts +++ b/src/modules/user/pages/UserProfile.spec.ts @@ -1,5 +1,5 @@ /* Testing imports */ -import { createLocalVue, mount } from "@vue/test-utils"; +import { type Wrapper, createLocalVue, mount } from "@vue/test-utils"; import { createTestingPinia } from "@pinia/testing"; /* Vue i18n */ @@ -23,122 +23,181 @@ import Multiselect from "@/plugins/deprecated/Multiselect.vue"; import UserProfile from "./UserProfile.vue"; import MergeUserModal from "./modals/MergeUserModal.vue"; import { - testInstitute, - testOrganization, - getTestUserState, + testInstituteFromShibboleth, + testOrganizationFromShibboleth, + getTestShibbolethUserState, + getTestOrcidUserState, } from "@/data/mockup/testUser"; -import useUserStore from "@/modules/user/store"; import type Vue from "vue"; +import type { Validation, ValidationArgs } from "@vuelidate/core"; import useLoginStore from "@/modules/login/store"; +import { UserForUpdateDto } from "@coscine/api-client/dist/types/Coscine.Api"; /* Create a local Vue instance */ const localVue = createLocalVue(); localVue.use(PiniaVuePlugin); localVue.use(BootstrapVue); +// Define the Vue instance type (computed properties) +interface UserProfileComponent extends Vue { + v$: Validation<ValidationArgs<{ userForUpdate: UserForUpdateDto }>, unknown>; +} + describe("UserProfile.vue", () => { localVue.component("Multiselect", Multiselect); + let wrapper: Wrapper<UserProfileComponent>; - /* Checks for correct button validation for external users */ - test("externalUser", async () => { - const testUserState = getTestUserState(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUserState.user!.organizations = []; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUserState.user!.institutes = []; - const wrapper = mount(UserProfile as unknown as typeof Vue, { - pinia: createTestingPinia({ - createSpy: vitest.fn, - initialState: { - user: testUserState, - }, - }), + /* Checks for correct button validation for ORCiD (external) users */ + test("ORCiD (external) User", async () => { + // Prepare the initial state of the user store + const testUserState = getTestOrcidUserState(); + if (!testUserState.user) { + throw new Error("Test user is null or undefined!"); + } + // Clear the organizations and institutes to test the validation + testUserState.user.organizations = []; + testUserState.user.institutes = []; + // Create a mocked pinia instance with initial state + const testingPinia = createTestingPinia({ + createSpy: vitest.fn, + initialState: { + user: testUserState, + }, + }); + // Mount the component + wrapper = mount(UserProfile as unknown as typeof Vue, { + pinia: testingPinia, i18n, localVue, - }); + }) as Wrapper<UserProfileComponent>; - expect(wrapper.get("#saveBtn").attributes()["disabled"]).toBe("disabled"); + /* + * NOTE: This test might fail if any of the other validation property is invalid! + * Ensure here that the rest of the properties inside the validation v$ are valid. + */ + expect(wrapper.vm.v$.userForUpdate.organization.$invalid).toBeTruthy(); + expect(wrapper.vm.v$.userForUpdate.institute.$invalid).toBeTruthy(); + // Save button should be disabled, since neither organization nor institute is set + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeTruthy(); /* Organization */ const element = wrapper.findComponent({ ref: "organization", }); expect(element.exists()).toBeTruthy(); - (element.vm as unknown as typeof Multiselect).select(testOrganization.name); + (element.vm as unknown as typeof Multiselect).select( + testOrganizationFromShibboleth.displayName + ); expect(wrapper.vm.$data.userForUpdate.organization).toBe( - testOrganization.name + testOrganizationFromShibboleth.displayName ); - expect(wrapper.get("#saveBtn").attributes()["disabled"]).toBe("disabled"); + expect(wrapper.vm.v$.userForUpdate.organization.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.institute.$invalid).toBeTruthy(); + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeTruthy(); /* Institute */ await wrapper.get("#institute").setValue("Test Institute"); await wrapper.vm.$nextTick(); expect(wrapper.vm.$data.userForUpdate.institute).toBe("Test Institute"); - /* Active Save Button since every condition is fulfilled */ - expect(wrapper.get("#saveBtn").attributes()["disabled"]).not.toBe( - "disabled" - ); + /* Active Save Button since every condition is fulfilled and the entire form is valid */ + expect(wrapper.vm.v$.userForUpdate.organization.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.institute.$invalid).toBeFalsy(); + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeFalsy(); }); /* Checks for correct button validation for internal users with only organization */ - test("internalUserOnlyOrg", async () => { - const testUserState = getTestUserState(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUserState.user!.institutes = []; - const wrapper = mount(UserProfile as unknown as typeof Vue, { - pinia: createTestingPinia({ - createSpy: vitest.fn, - initialState: { - user: testUserState, - }, - }), + test("Shibboleth (internal) User with only an organization set", async () => { + // Prepare the initial state of the user store + const testUserState = getTestShibbolethUserState(); + if (!testUserState.user) { + throw new Error("Test user is null or undefined!"); + } + // Clear the organizations and institutes to test the validation + testUserState.user.institutes = []; + // Create a mocked pinia instance with initial state + const testingPinia = createTestingPinia({ + createSpy: vitest.fn, + initialState: { + user: testUserState, + }, + }); + // Mount the component + wrapper = mount(UserProfile as unknown as typeof Vue, { + pinia: testingPinia, i18n, localVue, - }); + }) as Wrapper<UserProfileComponent>; - /* Save button disabled, since institution is not set */ - expect(wrapper.get("#saveBtn").attributes()["disabled"]).toBe("disabled"); + /* + * NOTE: This test might fail if any of the other validation property is invalid! + * Ensure here that the rest of the properties inside the validation v$ are valid. + */ + expect(wrapper.vm.v$.userForUpdate.organization.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.institute.$invalid).toBeTruthy(); + // Save button should be disabled, since institute is not set + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeTruthy(); - /* Organization */ - const element = wrapper.findComponent({ + /* Organization dropdown */ + const organizationDropdown = wrapper.findComponent({ ref: "organization", }); - expect(element.exists()).toBeTruthy(); - expect((element.vm as unknown as typeof Multiselect).$props.disabled).toBe( - true - ); + expect(organizationDropdown.exists()).toBeTruthy(); + // Shibboleth users' organization is always read-only, thus the dropdown should be disabled + expect( + (organizationDropdown.vm as unknown as typeof Multiselect).$props.disabled + ).toBeTruthy(); - /* Institute */ + /* Institute field */ await wrapper.get("#institute").setValue("Test Institute"); await wrapper.vm.$nextTick(); expect(wrapper.vm.$data.userForUpdate.institute).toBe("Test Institute"); - /* Active Save Button since every condition is fulfilled */ - expect(wrapper.get("#saveBtn").attributes()["disabled"]).not.toBe( - "disabled" - ); + /* Active Save Button since every condition is fulfilled and the entire form is valid */ + expect(wrapper.vm.v$.userForUpdate.organization.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.institute.$invalid).toBeFalsy(); + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeFalsy(); }); /* Checks for correct button validation for internal users */ - test("internalUser", async () => { - const testUserState = getTestUserState(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUserState.user!.organizations = [testOrganization, testInstitute]; - const wrapper = mount(UserProfile as unknown as typeof Vue, { - pinia: createTestingPinia({ - createSpy: vitest.fn, - initialState: { - user: testUserState, - }, - }), + test("Shibboleth (internal) User email changed", async () => { + // Prepare the initial state of the user store + const testUserState = getTestShibbolethUserState(); + if (!testUserState.user) { + throw new Error("Test user is null or undefined!"); + } + // Create a mocked pinia instance with initial state + const testingPinia = createTestingPinia({ + createSpy: vitest.fn, + initialState: { + user: testUserState, + }, + }); + // Mount the component + wrapper = mount(UserProfile as unknown as typeof Vue, { + pinia: testingPinia, i18n, localVue, - }); + }) as Wrapper<UserProfileComponent>; /* Save button disabled, since nothing has changed */ - expect(wrapper.get("#saveBtn").attributes()["disabled"]).toBe("disabled"); + expect(wrapper.vm.v$.userForUpdate.email.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.organization.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.institute.$invalid).toBeFalsy(); + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeTruthy(); /* Organization */ const element = wrapper.findComponent({ @@ -150,25 +209,30 @@ describe("UserProfile.vue", () => { ); /* Email */ - const userStore = useUserStore(); - expect(userStore.user?.email).toBe("example@example.com"); - expect(wrapper.vm.$data.userForUpdate.email).toBe("example@example.com"); + expect(wrapper.vm.$data.userForUpdate.email).toBe("example@university.com"); + expect(wrapper.vm.v$.userForUpdate.email.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.email.$anyDirty).toBeFalsy(); await wrapper.get("#email").setValue("servicedesk@itc.rwth-aachen.de"); expect(wrapper.vm.$data.userForUpdate.email).toBe( "servicedesk@itc.rwth-aachen.de" ); + expect(wrapper.vm.v$.userForUpdate.email.$invalid).toBeFalsy(); + expect(wrapper.vm.v$.userForUpdate.email.$anyDirty).toBeTruthy(); /* Active Save Button since every condition is fulfilled */ - expect(wrapper.get("#saveBtn").attributes()["disabled"]).not.toBe( - "disabled" - ); + expect( + (wrapper.get("#saveBtn").element as HTMLButtonElement).disabled + ).toBeFalsy(); }); /* Checks for correct ORCID connect button behavior */ - test("orcidConnection", async () => { - const testUserState = getTestUserState(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUserState.user!.organizations = [testOrganization, testInstitute]; + test("ORCID connect button triggers user merge modal and logout action", async () => { + const testUserState = getTestShibbolethUserState(); + if (!testUserState.user) { + throw new Error("Test user is null or undefined!"); + } + testUserState.user.organizations = [testOrganizationFromShibboleth]; + testUserState.user.institutes = [testInstituteFromShibboleth]; /* Assign the test pinia store to a variable to use later */ const testingPinia = createTestingPinia({ createSpy: vitest.fn, @@ -193,7 +257,7 @@ describe("UserProfile.vue", () => { // Assert modal appeared const modal = wrapper.findComponent(MergeUserModal); - expect(modal.exists()).toBe(true); + expect(modal.exists()).toBeTruthy(); // Find and click logout button const logoutBtn = modal.find("#logOutForMerge"); diff --git a/src/modules/user/pages/UserProfile.vue b/src/modules/user/pages/UserProfile.vue index db5e1616dfd8015df00261fdd62b0490a80ccbb0..a99c685a8deb819fb3b54a044e56ae7fa9ee525d 100644 --- a/src/modules/user/pages/UserProfile.vue +++ b/src/modules/user/pages/UserProfile.vue @@ -57,7 +57,7 @@ <!-- First Name --> <CoscineFormGroup :mandatory="true" - label-for="firstName" + label-for="givenName" :label=" $t( 'page.userprofile.form.personalInformation.labels.firstNameLabel' @@ -67,15 +67,15 @@ type="input" > <b-form-input - id="firstName" - v-model="v$.userForUpdate.firstName.$model" + id="givenName" + v-model="v$.userForUpdate.givenName.$model" :state=" - v$.userForUpdate.firstName.$dirty - ? !v$.userForUpdate.firstName.$invalid + v$.userForUpdate.givenName.$dirty + ? !v$.userForUpdate.givenName.$invalid : null " :placeholder=" - $t('page.userprofile.form.personalInformation.firstName') + $t('page.userprofile.form.personalInformation.givenName') " /> </CoscineFormGroup> @@ -83,7 +83,7 @@ <!-- Last Name --> <CoscineFormGroup :mandatory="true" - label-for="lastName" + label-for="familyName" :label=" $t('page.userprofile.form.personalInformation.labels.lastNameLabel') " @@ -91,15 +91,15 @@ type="input" > <b-form-input - id="lastName" - v-model="v$.userForUpdate.lastName.$model" + id="familyName" + v-model="v$.userForUpdate.familyName.$model" :state=" - v$.userForUpdate.lastName.$dirty - ? !v$.userForUpdate.lastName.$invalid + v$.userForUpdate.familyName.$dirty + ? !v$.userForUpdate.familyName.$invalid : null " :placeholder=" - $t('page.userprofile.form.personalInformation.lastName') + $t('page.userprofile.form.personalInformation.familyName') " /> </CoscineFormGroup> @@ -114,17 +114,21 @@ :is-loading="isLoading" type="input" > - <b-form-input - id="email" - v-model="v$.userForUpdate.email.$model" - :state=" - v$.userForUpdate.email.$dirty || !userForUpdate.email - ? !v$.userForUpdate.email.$invalid - : null - " - :placeholder="$t('page.userprofile.form.personalInformation.email')" - /> - <div id="emailHint">{{ emailHint }}</div> + <div class="w-100"> + <b-form-input + id="email" + v-model="v$.userForUpdate.email.$model" + :state=" + v$.userForUpdate.email.$dirty || !userForUpdate.email + ? !v$.userForUpdate.email.$invalid + : null + " + :placeholder=" + $t('page.userprofile.form.personalInformation.email') + " + /> + <div id="emailHint">{{ emailHint }}</div> + </div> </CoscineFormGroup> <!-- Organization --> @@ -147,7 +151,7 @@ v-model="v$.userForUpdate.organization.$model" :options="ror" :multiple="false" - :loading="loadingOrganizations" + :loading="isLoadingOrganizations" :show-labels="false" :placeholder=" $t( @@ -156,12 +160,31 @@ " @search-change="triggerFetchOptions" > - <template slot="noOptions"> - {{ - $t( - "page.userprofile.form.personalInformation.multiselect.noOptionsOrganization" - ) - }} + <template #noResult="props"> + <span v-if="props.search.length < searchCharThreshold"> + {{ + $t( + "page.userprofile.form.personalInformation.multiselect.searchNotEnoughCharacters", + { min: searchCharThreshold } + ) + }} + </span> + <span v-else> + {{ + $t( + "page.userprofile.form.personalInformation.multiselect.noResults" + ) + }} + </span> + </template> + <template #noOptions> + <span v-if="!isLoadingOrganizations"> + {{ + $t( + "page.userprofile.form.personalInformation.multiselect.noOptionsOrganization" + ) + }} + </span> </template> </multiselect> @@ -318,10 +341,9 @@ > <b-button variant="secondary" - class="float-left" name="orcidConnect" :disabled="orcidConnected" - @click.prevent="clickConnect(externalAuthenticators.OrciD)" + @click.prevent="clickOrcidConnect()" > <img class="d-inline mr-1" @@ -345,10 +367,9 @@ > <b-button variant="secondary" - class="float-left" name="shibbolethConnect" :disabled="shibbolethConnected" - @click.prevent="clickConnect(externalAuthenticators.Shibboleth)" + @click.prevent="clickSsoConnect()" > {{ shibbolethConnected @@ -365,7 +386,7 @@ ref="saveBtn" type="submit" variant="primary" - class="float-right" + class="ml-auto" name="save" :disabled=" v$.userForUpdate.$invalid || @@ -401,12 +422,12 @@ import useLoginStore from "@/modules/login/store"; import useNotificationStore from "@/store/notification"; import type { DisciplineDto, + IdentityProviders, LanguageDto, TitleDto, UserDto, UserForUpdateDto, } from "@coscine/api-client/dist/types/Coscine.Api/api"; -import { ExternalAuthenticators } from "../types"; import { UserDto2UserForUpdateDto, userMapper } from "@/mapping/user"; export default defineComponent({ @@ -424,22 +445,22 @@ export default defineComponent({ return { userForUpdate: { title: {}, // This needs to be nullified for the multiselect's placeholder - firstName: "", - lastName: "", + givenName: "", + familyName: "", email: "", organization: "", institute: "", disciplines: [] as DisciplineDto[], language: {}, } as UserForUpdateDto, + searchCharThreshold: 3, isLoading: false, savingProfile: false, currentUserComponent: "UserProfileComponent", selectedExternalOrganization: null as Record<string, unknown> | null, queryTimer: 0, - loadingOrganizations: false, - externalAuthenticators: ExternalAuthenticators, - providerToMerge: null as ExternalAuthenticators | null, + isLoadingOrganizations: false, + providerToMerge: null as IdentityProviders | null, }; }, computed: { @@ -460,7 +481,7 @@ export default defineComponent({ return this.$t( "page.userprofile.form.personalInformation.emailChange.noAddress" ).toString(); - } else if (!this.user?.isEmailConfirmed) { + } else if (!this.user?.emails?.some((e) => e.isConfirmed)) { return this.$t( "page.userprofile.form.personalInformation.emailChange.pendingConfirmation" ).toString(); @@ -470,7 +491,7 @@ export default defineComponent({ institutes(): string[] { if (this.user?.institutes) { return this.user.institutes - .map((org) => (org.name ? org.name : "")) // Extract organization display name, could contain empty strings + .map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings .filter((n) => n); // Filter out empty strings, if any;; } return []; @@ -479,11 +500,11 @@ export default defineComponent({ return this.userStore.userProfile.languages; }, orcidConnected(): boolean { - if (this.user?.externalAuthenticators) { - return this.user.externalAuthenticators.some( - (externalAuthenticator) => - externalAuthenticator.displayName && - externalAuthenticator.displayName.toLowerCase() === "orcid" + if (this.user?.identities) { + return this.user.identities.some( + (idProvider) => + idProvider.displayName && + idProvider.displayName.toLowerCase() === "orcid" ); } return false; @@ -491,7 +512,7 @@ export default defineComponent({ organizations(): string[] { if (this.user?.organizations) { return this.user?.organizations - .map((org) => (org.name ? org.name : "")) // Extract organization display name, could contain empty strings + .map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings .filter((n) => n); // Filter out empty strings, if any; } return []; @@ -506,18 +527,18 @@ export default defineComponent({ const organizations = this.userStore.userProfile.organizations; if (organizations) { return organizations - .map((org) => (org.name ? org.name : "")) // Extract organization display name, could contain empty strings + .map((org) => (org.displayName ? org.displayName : "")) // Extract organization display name, could contain empty strings .filter((n) => n); // Filter out empty strings, if any } else { return []; } }, shibbolethConnected(): boolean { - if (this.user?.externalAuthenticators) { - return this.user?.externalAuthenticators.some( - (externalAuthenticator) => - externalAuthenticator.displayName && - externalAuthenticator.displayName.toLowerCase() === "shibboleth" + if (this.user?.identities) { + return this.user?.identities.some( + (idProvider) => + idProvider.displayName && + idProvider.displayName.toLowerCase() === "shibboleth" ); } return false; @@ -539,8 +560,8 @@ export default defineComponent({ return { userForUpdate: { title: {}, - firstName: { required }, - lastName: { required }, + givenName: { required }, + familyName: { required }, email: { email, required }, organization: { required, organizationValidator }, institute: { required, instituteValidator }, @@ -573,7 +594,13 @@ export default defineComponent({ } } }, - async clickConnect(provider: ExternalAuthenticators) { + async clickOrcidConnect() { + await this.clickConnect("OrciD" as IdentityProviders.OrciD); + }, + async clickSsoConnect() { + await this.clickConnect("Shibboleth" as IdentityProviders.Shibboleth); + }, + async clickConnect(provider: IdentityProviders) { this.providerToMerge = provider; this.$bvModal.show("mergeUserModal"); }, @@ -603,18 +630,18 @@ export default defineComponent({ }, triggerFetchOptions(search: string) { clearTimeout(this.queryTimer); - if (search.length < 3) { + if (search.length < this.searchCharThreshold) { return; } + this.isLoadingOrganizations = true; // Set the flag here, to avoid delayed loading indication this.queryTimer = window.setTimeout( () => this.fetchOrganizationOptions(search), 1000 ); }, async fetchOrganizationOptions(search: string) { - this.loadingOrganizations = true; await this.userStore.retrieveOrganizations(search); - this.loadingOrganizations = false; + this.isLoadingOrganizations = false; }, }, }); diff --git a/src/modules/user/pages/modals/MergeUserModal.vue b/src/modules/user/pages/modals/MergeUserModal.vue index f081af403226ab5584f18148402dbf45b5af32e7..533cf4b60cb5edc24766929c0e3d6c704057895c 100644 --- a/src/modules/user/pages/modals/MergeUserModal.vue +++ b/src/modules/user/pages/modals/MergeUserModal.vue @@ -59,13 +59,13 @@ import { defineComponent, type PropType } from "vue"; import useLoginStore from "@/modules/login/store"; import useUserStore from "../../store"; -import { ExternalAuthenticators } from "../../types"; +import type { IdentityProviders } from "@coscine/api-client/dist/types/Coscine.Api"; export default defineComponent({ props: { providerToMerge: { default: null, - type: String as PropType<ExternalAuthenticators | null>, + type: String as PropType<IdentityProviders | null>, }, }, setup() { diff --git a/src/modules/user/store.ts b/src/modules/user/store.ts index 68ff508c72826269ea9817b43f63cb15658479a3..b58305ff0c4205057b2e7c4a4910f6ee618857f5 100644 --- a/src/modules/user/store.ts +++ b/src/modules/user/store.ts @@ -1,8 +1,9 @@ import { LanguageApi, OrganizationApi, + SelfApi, + SelfApiTokenApi, TitleApi, - TokenApi, UserApi, } from "@coscine/api-client"; import { defineStore } from "pinia"; @@ -18,7 +19,7 @@ import { removeQueryParameterFromUrl } from "@/router"; import { DisciplineApi } from "@coscine/api-client"; import type { ApiTokenForCreationDto, - ExternalAuthenticators, + IdentityProviders, UserForUpdateDto, UserMergeDto, } from "@coscine/api-client/dist/types/Coscine.Api"; @@ -98,7 +99,7 @@ export const useUserStore = defineStore({ try { const confirmationToken = route.query[tokenKey]?.toString(); if (confirmationToken) { - await UserApi.confirmUserEmail(confirmationToken); + await SelfApi.confirmUserEmail(confirmationToken); notificationStore.postNotification({ title: i18n.t("toast.contactChange.success.title").toString(), body: i18n.t("toast.contactChange.success.message").toString(), @@ -171,7 +172,7 @@ export const useUserStore = defineStore({ async retrieveTokens() { const notificationStore = useNotificationStore(); try { - let apiResponse = await TokenApi.getAllApiTokens(); + let apiResponse = await SelfApiTokenApi.getAllApiTokens(); if (apiResponse.data.data) { // Assign the data from the first page this.userProfile.tokens = [...apiResponse.data.data]; @@ -186,7 +187,7 @@ export const useUserStore = defineStore({ ? apiResponse.data.pagination.currentPage + 1 : 1; // Retrieve the next page - apiResponse = await TokenApi.getAllApiTokens(nextPage); + apiResponse = await SelfApiTokenApi.getAllApiTokens(nextPage); if (apiResponse.data.data) { // Extend the data with the newly fetched data this.userProfile.tokens = [...apiResponse.data.data]; @@ -204,7 +205,7 @@ export const useUserStore = defineStore({ async retrieveUser() { const notificationStore = useNotificationStore(); try { - const apiResponse = await UserApi.getCurrentUser(); + const apiResponse = await SelfApi.getCurrentUser(); this.user = apiResponse.data.data; } catch (error) { // Handle other Status Codes @@ -215,7 +216,7 @@ export const useUserStore = defineStore({ async deleteToken(tokenId: string) { const notificationStore = useNotificationStore(); try { - await TokenApi.revokeToken(tokenId); + await SelfApiTokenApi.revokeToken(tokenId); } catch (error) { // Handle other Status Codes notificationStore.postApiErrorNotification(error as AxiosError); @@ -223,11 +224,11 @@ export const useUserStore = defineStore({ }, async initiateUserMerge( - provider: ExternalAuthenticators + provider: IdentityProviders ): Promise<UserMergeDto | null | undefined> { const notificationStore = useNotificationStore(); try { - const apiResponse = await UserApi.initiateUserMerge(provider); + const apiResponse = await SelfApi.initiateUserMerge(provider); return apiResponse.data.data; } catch (error) { // Handle other Status Codes @@ -239,7 +240,9 @@ export const useUserStore = defineStore({ async createApiToken(tokenForCreation: ApiTokenForCreationDto) { const notificationStore = useNotificationStore(); try { - const apiResponse = await TokenApi.createApiToken(tokenForCreation); + const apiResponse = await SelfApiTokenApi.createApiToken( + tokenForCreation + ); return apiResponse.data.data; } catch (error) { // Handle other Status Codes @@ -250,7 +253,7 @@ export const useUserStore = defineStore({ async updateUser(userForUpdateDto: UserForUpdateDto): Promise<boolean> { const notificationStore = useNotificationStore(); try { - await UserApi.updateCurrentUser(userForUpdateDto); + await SelfApi.updateCurrentUser(userForUpdateDto); return true; } catch (error) { // Handle other Status Codes diff --git a/src/modules/user/types.ts b/src/modules/user/types.ts index 86888fd0931b7c78bc20ed483d5181e6348599a2..24092a0c008cf3e7bc3ddccc5c13a09801e37bbd 100644 --- a/src/modules/user/types.ts +++ b/src/modules/user/types.ts @@ -29,9 +29,9 @@ export interface TokenValidityBoundDates { } /** - * Defining the {@link ExternalAuthenticators} enum inside the types, because it is not being exported correctly from the @coscine/api-client library. + * Defining the {@link IdentityProviders} enum inside the types, because it is not being exported correctly from the @coscine/api-client library. */ -export enum ExternalAuthenticators { +export enum IdentityProviders { Shibboleth = "Shibboleth", OrciD = "ORCiD", } diff --git a/src/plugins/mockupPinia.ts b/src/plugins/mockupPinia.ts index 7b00a241478aae41962de4358104a44dc59f8aca..5669bb52de256634fae47a0bba1493c72833b596 100644 --- a/src/plugins/mockupPinia.ts +++ b/src/plugins/mockupPinia.ts @@ -1,6 +1,6 @@ import { createTestingPinia } from "@pinia/testing"; import { testProjectState } from "@/data/mockup/testProject"; -import { getTestUserState } from "@/data/mockup/testUser"; +import { getTestShibbolethUserState } from "@/data/mockup/testUser"; import { getTestResourceState } from "@/data/mockup/testResource"; import { testLoginState } from "@/data/mockup/testLogin"; import { getTestMainState } from "@/data/mockup/testMain"; @@ -14,7 +14,7 @@ const pinia = createTestingPinia({ project: testProjectState, resource: await getTestResourceState(), search: testSearchState, - user: getTestUserState(), + user: getTestShibbolethUserState(), }, }); diff --git a/src/store/index.ts b/src/store/index.ts index 4df0ff57184a8d89ddd13045da0a51d0e1e7e176..ef6032244a8265b8cf3305b84150c28a2f92d2f0 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -105,8 +105,9 @@ export const useMainStore = defineStore({ }, async getMaintenance() { - const apiResponse = await MaintenanceApi.getCurrentMaintenance(); - const maintenance = apiResponse.data.data; + const apiResponse = await MaintenanceApi.getCurrentMaintenances(); + // TODO: Make it work with multiple maintenance messages + const maintenance = apiResponse.data.data?.at(0); // Take the first maintenance if (maintenance?.startsDate) { const now = new Date(Date.now()); const startDate = new Date(maintenance.startsDate); diff --git a/src/store/notification.ts b/src/store/notification.ts index cca0fe9ee6c3fdb110f1bc42ed42d7590d44180b..4f215f532cade98efc1d1f0a3e97dc6bb2e0c30b 100644 --- a/src/store/notification.ts +++ b/src/store/notification.ts @@ -83,7 +83,7 @@ export const useNotificationStore = defineStore({ }, postNotification(toast: NotificationToast) { - this.notificationQueue.push(toast); + this.notificationQueue.push(toast); // TODO: Address this warning }, deleteNotification(toast: NotificationToast) { diff --git a/src/store/types.ts b/src/store/types.ts index 2a3751dd667c4880babcb3d0308f596b8bd4b832..33c8674478883f9a39405167130e53e8456da921 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -2,10 +2,9 @@ import type { RemovableRef } from "@vueuse/core"; import type { MaintenanceDto } from "@coscine/api-client/dist/types/Coscine.Api"; import type { VNode } from "vue"; import type { BvToastOptions } from "bootstrap-vue"; -import type { TranslateResult } from "vue-i18n"; export interface NotificationToast extends BvToastOptions { - body: string | TranslateResult | VNode | Array<VNode>; + body: string | VNode | Array<VNode>; variant?: "danger" | "warning" | "success" | "info" | "primary" | "secondary"; }