From 6702858617dcdb43290a57df4a9183438d940a9f Mon Sep 17 00:00:00 2001 From: GaziYucel Date: Wed, 5 Feb 2025 17:07:47 +0100 Subject: [PATCH] pkp/pkp-lib#7135 Multiple author affiliations (Ror) --- public/globals.js | 17 + src/components/Form/FormGroup.vue | 4 + .../Form/fields/FieldAffiliations.mdx | 23 + .../Form/fields/FieldAffiliations.stories.js | 47 ++ .../Form/fields/FieldAffiliations.vue | 531 ++++++++++++++++++ ...FieldAffiliationsRorAutoSuggest.stories.js | 33 ++ ...ue => FieldAffiliationsRorAutoSuggest.vue} | 46 +- .../fields/FieldRorAutosuggest.stories.js | 40 -- .../Form/mocks/field-affiliations.js | 425 ++++++++++++++ .../contributors/ContributorsListPanel.vue | 3 + 10 files changed, 1116 insertions(+), 53 deletions(-) create mode 100644 src/components/Form/fields/FieldAffiliations.mdx create mode 100644 src/components/Form/fields/FieldAffiliations.stories.js create mode 100644 src/components/Form/fields/FieldAffiliations.vue create mode 100644 src/components/Form/fields/FieldAffiliationsRorAutoSuggest.stories.js rename src/components/Form/fields/{FieldRorAutosuggest.vue => FieldAffiliationsRorAutoSuggest.vue} (77%) delete mode 100644 src/components/Form/fields/FieldRorAutosuggest.stories.js create mode 100644 src/components/Form/mocks/field-affiliations.js diff --git a/public/globals.js b/public/globals.js index 9e0dbf984..68665e459 100644 --- a/public/globals.js +++ b/public/globals.js @@ -158,6 +158,7 @@ window.pkp = { 'article.metadata': 'Metadata', 'author.users.contributor.principalContact': 'Primary Contact', 'author.users.contributor.setPrincipalContact': 'Set Primary Contact', + 'common.add': 'Add', 'common.addCCBCC': 'Add CC/BCC', 'common.assign': 'Assign', 'common.attachFiles': 'Attach Files', @@ -811,6 +812,22 @@ window.pkp = { 'The submission has been advanced to the next round of review', 'workflow.submissionNextReviewRoundInFutureStage': 'The submission advanced to the next review round, was accepted, and is currently in the {$stage} stage.', + 'user.affiliations': 'Affiliations', + 'user.affiliations.description': 'Enter the full name of the institution below, avoiding any acronyms. Select the name from the dropdown and click "Add" to include the affiliation in your profile. (e.g. "Simon Fraser University")', + 'user.affiliations.institution': 'Institution', + 'user.affiliations.translation': 'More information', + 'user.affiliations.translationEditActionLabel': 'Edit institution name', + 'user.affiliations.translationDeleteActionLabel': 'Remove institution', + 'user.affiliations.translationActionsAriaLabel': 'Click to edit or delete', + 'user.affiliations.translationsAllAvailable': 'All translations available', + 'user.affiliations.translationsSomeAvailable': '{$count} of {$total} languages completed', + 'user.affiliations.typeTranslationNameInLanguageLabel': 'Type the institute name in {$language}', + 'user.affiliations.translationNameInLanguage': 'Institute name in {$language}', + 'user.affiliations.deleteModal.title': 'Are you sure?', + 'user.affiliations.deleteModal.message': 'The affiliation {$affiliation} will be deleted.', + 'user.affiliations.searchPhraseLabel': 'Type the institute name in {$language}', + 'user.affiliations.searchPhraseNothingFound': 'Your search phrase could not be found', + 'user.affiliations.primaryLocaleRequired': 'The primary language {$primaryLocale} is required', }, tinyMCE: { skinUrl: '/styles/tinymce', diff --git a/src/components/Form/FormGroup.vue b/src/components/Form/FormGroup.vue index ce7b6087c..730224d66 100644 --- a/src/components/Form/FormGroup.vue +++ b/src/components/Form/FormGroup.vue @@ -43,6 +43,8 @@ :key="field.name" :all-errors="errors" :form-id="formId" + :primary-locale="primaryLocale" + :locales="availableLocales" @change="fieldChanged" @set-errors="setFieldErrors" > @@ -53,6 +55,7 @@ diff --git a/src/components/Form/fields/FieldAffiliationsRorAutoSuggest.stories.js b/src/components/Form/fields/FieldAffiliationsRorAutoSuggest.stories.js new file mode 100644 index 000000000..106dc0337 --- /dev/null +++ b/src/components/Form/fields/FieldAffiliationsRorAutoSuggest.stories.js @@ -0,0 +1,33 @@ +import {http, HttpResponse} from 'msw'; +import FieldAffiliationsRorAutoSuggest from './FieldAffiliationsRorAutoSuggest.vue'; +import FieldAffiliationsMock from '@/components/Form/mocks/field-affiliations'; + +export default { + title: 'Forms/FieldAffiliationsRorAutoSuggest', + component: FieldAffiliationsRorAutoSuggest, + render: (args) => ({ + components: {FieldAffiliationsRorAutoSuggest}, + setup() { + return {args}; + }, + template: '', + }), + parameters: { + msw: { + handlers: [ + http.get('https://api.ror.org/v2/organizations', async () => { + return HttpResponse.json(FieldAffiliationsMock.organizations); + }), + ], + }, + docs: { + story: { + height: '500px', + }, + }, + }, +}; + +export const Default = { + args: {...FieldAffiliationsMock}, +}; diff --git a/src/components/Form/fields/FieldRorAutosuggest.vue b/src/components/Form/fields/FieldAffiliationsRorAutoSuggest.vue similarity index 77% rename from src/components/Form/fields/FieldRorAutosuggest.vue rename to src/components/Form/fields/FieldAffiliationsRorAutoSuggest.vue index 356f3cfae..e6776e0bb 100644 --- a/src/components/Form/fields/FieldRorAutosuggest.vue +++ b/src/components/Form/fields/FieldAffiliationsRorAutoSuggest.vue @@ -61,7 +61,7 @@ import {useFetch} from '@/composables/useFetch'; import {useId} from '@/composables/useId'; const {generateId} = useId(); - +const autosuggestContainerId = generateId(); const currentSelected = ref([]); const inputValue = ref(''); const isFocused = ref(false); @@ -69,8 +69,7 @@ const url = 'https://api.ror.org/v2/organizations'; const queryParams = computed(() => ({ query: inputValue.value, })); - -const autosuggestContainerId = generateId(); +const noLangCode = 'no_lang_code'; const { data: suggestions, @@ -97,14 +96,29 @@ const autoSuggestProps = computed(() => ({ const mappedSuggestions = computed(() => { return suggestions.value?.items.map((item) => { - // get the name from "ror_display" type - const name = item.names?.find((i) => - i.types.includes('ror_display'), - )?.value; + const displayLocale = + item.names?.find((i) => i.types.includes('ror_display'))?.lang !== null + ? item.names?.find((i) => i.types.includes('ror_display'))?.lang + : noLangCode; + + let names = {}; + item.names?.forEach((name) => { + if (name.types.includes('label') || name.types.includes('ror_display')) { + const locale = name.lang !== null ? name.lang : noLangCode; + names[locale] = name.value; + } + }); return { - value: item.id, - label: name, + value: { + id: null, + ror: item.id, + displayLocale: displayLocale, + isActive: item.status === 'active' ? 1 : 0, + name: names, + _href: null, + }, + label: names[displayLocale], hasSlot: true, href: item.id, }; @@ -112,7 +126,9 @@ const mappedSuggestions = computed(() => { }); watch(queryParams, () => { - getSuggestions(); + if (inputValue.value.length > 3) { + getSuggestions(); + } }); async function getSuggestions() { @@ -129,7 +145,7 @@ function changeFocus(focused) { function handleSelect(suggestion) { if (!suggestion) { - if (!inputValue.value || !mappedSuggestions.value.length) { + if (!inputValue.value) { return; } @@ -139,8 +155,8 @@ function handleSelect(suggestion) { }; } - if (!currentSelected.value.some((item) => item.value === suggestion.value)) { - currentSelected.value.push(suggestion); + if (currentSelected.value !== suggestion.value) { + currentSelected.value = [suggestion]; } } @@ -149,4 +165,8 @@ function handleDeselect(item) { (selected) => selected.value !== item.value, ); } + +defineExpose({ + currentSelected, +}); diff --git a/src/components/Form/fields/FieldRorAutosuggest.stories.js b/src/components/Form/fields/FieldRorAutosuggest.stories.js deleted file mode 100644 index 68a75679f..000000000 --- a/src/components/Form/fields/FieldRorAutosuggest.stories.js +++ /dev/null @@ -1,40 +0,0 @@ -import {http, HttpResponse} from 'msw'; -import FieldRorAutosuggest from './FieldRorAutosuggest.vue'; -import InstitutionsMock from '@/mocks/institutions.json'; - -export default { - title: 'Components/FieldRorAutosuggest', - component: FieldRorAutosuggest, - render: (args) => ({ - components: {FieldRorAutosuggest}, - setup() { - return {args}; - }, - template: '', - }), - parameters: { - msw: { - handlers: [ - http.get('https://api.ror.org/v2/organizations', async () => { - return HttpResponse.json(InstitutionsMock); - }), - ], - }, - docs: { - story: { - height: '300px', - }, - }, - }, -}; - -export const Default = { - render: (args) => ({ - components: {FieldRorAutosuggest}, - setup() { - return {args}; - }, - template: '', - }), - args: {}, -}; diff --git a/src/components/Form/mocks/field-affiliations.js b/src/components/Form/mocks/field-affiliations.js new file mode 100644 index 000000000..48c4eed12 --- /dev/null +++ b/src/components/Form/mocks/field-affiliations.js @@ -0,0 +1,425 @@ +export default { + name: 'author-affiliations', + component: 'author-affiliations', + authorId: 1, + primaryLocale: 'en', + locales: [ + {key: 'en', label: 'English'}, + {key: 'fr_CA', label: 'French (Canada)'}, + {key: 'de', label: 'German'}, + ], + value: [ + { + id: 12, + authorId: 1, + ror: 'https://ror.org/0213rcc28', + name: { + en: 'Simon Fraser University', + fr_CA: 'Simon Fraser University', + de: 'Simon Fraser University', + tr: 'Simon Fraser Üniversitesi', + }, + }, + { + id: 13, + authorId: 1, + ror: 'https://ror.org/02e2c7k09', + name: { + en: 'Delft University of Technology', + fr_CA: '', + de: 'Technische Universität Delft', + tr: 'Delft Teknik Üniversitesi', + }, + }, + { + id: 14, + authorId: 1, + ror: '', + name: { + en: 'German National Library of Science and Technology', + fr_CA: '', + de: 'Technische Informationsbibliothek (TIB)', + tr: 'Teknik Bilgi Kütübanesi', + }, + }, + ], + organizations: { + number_of_results: 100, + time_taken: 15, + items: [ + { + id: 'https://ror.org/0213rcc28', + names: [ + { + lang: null, + types: ['acronym'], + value: 'SFU', + }, + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Simon Fraser University', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/02njbw696', + names: [ + { + lang: 'en', + types: ['label'], + value: 'Simón Bolívar University', + }, + { + lang: 'es', + types: ['ror_display', 'label'], + value: 'Universidad Simón Bolívar', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/01ak5cj98', + names: [ + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Simón Bolívar University', + }, + { + lang: null, + types: ['acronym'], + value: 'USB', + }, + { + lang: 'es', + types: ['label'], + value: 'Universidad Simón Bolívar', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/014579w63', + names: [ + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Fraser Health', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/03455j977', + names: [ + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Fraser Institute', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/00jtqvs23', + names: [ + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Fraser Research', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/04h6w7946', + names: [ + { + lang: 'en', + types: ['alias'], + value: 'Fraser Valley College', + }, + { + lang: null, + types: ['acronym'], + value: 'UFV', + }, + { + lang: 'en', + types: ['alias'], + value: 'University College of the Fraser Valley', + }, + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'University of the Fraser Valley', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/03z27es23', + names: [ + { + lang: null, + types: ['acronym'], + value: 'UMSS', + }, + { + lang: 'es', + types: ['label'], + value: 'Universidad Mayor de San Simón', + }, + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'University of San Simón', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/00wmrqg59', + names: [ + { + lang: null, + types: ['ror_display', 'label'], + value: 'West Fraser (Canada)', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/02bnf4x85', + names: [ + { + lang: null, + types: ['acronym'], + value: 'USK', + }, + { + lang: 'fr', + types: ['ror_display', 'label'], + value: 'Université Simon Kimbangu', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/04m9zwa74', + names: [ + { + lang: 'de', + types: ['ror_display', 'label'], + value: 'Claussen Simon Stiftung', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/03jrrzr69', + names: [ + { + lang: null, + types: ['acronym'], + value: 'UASB', + }, + { + lang: 'es', + types: ['ror_display', 'label'], + value: 'Universidad Andina Simón Bolívar', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/0208tyb13', + names: [ + { + lang: 'es', + types: ['ror_display', 'label'], + value: 'Universidad Peruana Simón Bolívar', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/02haar591', + names: [ + { + lang: null, + types: ['acronym'], + value: 'IPSL', + }, + { + lang: 'fr', + types: ['ror_display', 'label'], + value: 'Institut Pierre-Simon Laplace', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/00g1d7b60', + names: [ + { + lang: 'en', + types: ['alias'], + value: 'IU Cancer Center', + }, + { + lang: 'en', + types: ['alias'], + value: 'IU Melvin and Bren Simon Comprehensive Cancer Center', + }, + { + lang: 'en', + types: ['alias'], + value: 'IU Simon Cancer Center', + }, + { + lang: 'en', + types: ['alias'], + value: 'IU Simon Comprehensive Cancer Center', + }, + { + lang: 'en', + types: ['alias'], + value: 'Indiana University Cancer Center', + }, + { + lang: 'en', + types: ['ror_display', 'label'], + value: + 'Indiana University Melvin and Bren Simon Comprehensive Cancer Center', + }, + { + lang: 'en', + types: ['alias'], + value: 'Melvin and Bren Simon Comprehensive Cancer Center', + }, + { + lang: 'en', + types: ['alias'], + value: 'Simon Comprehensive Cancer Center', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/02anx2442', + names: [ + { + lang: null, + types: ['acronym'], + value: 'KhNUE', + }, + { + lang: 'en', + types: ['alias'], + value: 'Kharkiv National University of Economics', + }, + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Simon Kuznets Kharkiv National University of Economics', + }, + { + lang: null, + types: ['acronym'], + value: 'ХНЕУ', + }, + { + lang: 'uk', + types: ['label'], + value: 'Харківський національний економічний університет', + }, + { + lang: 'ru', + types: ['label'], + value: 'Харьковский национальный экономический университет', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/00nggaz43', + names: [ + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'Georg Simon Ohm University of Applied Sciences Nuremberg', + }, + { + lang: 'de', + types: ['alias'], + value: 'TH Nürnberg', + }, + { + lang: 'de', + types: ['label'], + value: 'Technische Hochschule Nürnberg Georg Simon Ohm', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/03951zg45', + names: [ + { + lang: null, + types: ['acronym'], + value: 'UNESR', + }, + { + lang: 'es', + types: ['ror_display', 'label'], + value: 'Universidad Nacional Experimental Simón Rodríguez', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/0407tnq23', + names: [ + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'John Simon Guggenheim Memorial Foundation', + }, + ], + status: 'active', + }, + { + id: 'https://ror.org/01a8ynn13', + names: [ + { + lang: 'en', + types: ['alias'], + value: 'Simon Langton Grammar School for Boys', + }, + { + lang: 'en', + types: ['alias'], + value: 'The Langton', + }, + { + lang: 'en', + types: ['ror_display', 'label'], + value: 'The Langton Grammar School for Boys', + }, + ], + status: 'active', + }, + ], + }, + newAffiliation: { + id: null, + authorId: 1, + ror: null, + name: {}, + rorObject: {}, + }, +}; diff --git a/src/components/ListPanel/contributors/ContributorsListPanel.vue b/src/components/ListPanel/contributors/ContributorsListPanel.vue index 129f15629..75854b96f 100644 --- a/src/components/ListPanel/contributors/ContributorsListPanel.vue +++ b/src/components/ListPanel/contributors/ContributorsListPanel.vue @@ -396,6 +396,9 @@ export default { field.isVerified = author['orcidIsVerified'] ?? false; field.orcidVerificationRequested = author['orcidVerificationRequested']; + } else if (field.name === 'affiliations') { + field.authorId = author['id']; + field.value = author[field.name]; } else if (Object.keys(author).includes(field.name)) { field.value = author[field.name]; }