diff --git a/public/globals.js b/public/globals.js index 3d90f042c..7b0b23921 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', @@ -804,6 +805,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/DropdownActions/DropdownActions.vue b/src/components/DropdownActions/DropdownActions.vue index 34b31b977..29d3b0f78 100644 --- a/src/components/DropdownActions/DropdownActions.vue +++ b/src/components/DropdownActions/DropdownActions.vue @@ -135,7 +135,9 @@ const emit = defineEmits([ ]); const emitAction = (action) => { - if (action.name) { + if (action.name && (action.id || action.id === 0)) { + emit('action', [action.name, action.id]); + } else if (action.name) { emit('action', action.name); } }; diff --git a/src/components/Form/Form.vue b/src/components/Form/Form.vue index cd3555845..55b34ad65 100644 --- a/src/components/Form/Form.vue +++ b/src/components/Form/Form.vue @@ -566,8 +566,17 @@ export default { '[id*="' + this.id + '-' + field.name + '"]', ); if ($el) { + // Handle scrolling within new side modals + const containers = document.querySelectorAll( + 'div.pkp-modal-scroll-container', + ); + const lastContainer = + containers.length > 0 + ? containers[containers.length - 1] + : undefined; this.$scrollTo($el, 500, { offset: -50, + container: lastContainer, }); } else { this.setCurrentPage(group.pageId); diff --git a/src/components/Form/FormGroup.vue b/src/components/Form/FormGroup.vue index ce7b6087c..843506640 100644 --- a/src/components/Form/FormGroup.vue +++ b/src/components/Form/FormGroup.vue @@ -53,6 +53,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..64d8c3126 100644 --- a/src/components/ListPanel/contributors/ContributorsListPanel.vue +++ b/src/components/ListPanel/contributors/ContributorsListPanel.vue @@ -294,6 +294,13 @@ export default { let activeForm = cloneDeep(this.form); activeForm.action = this.contributorsApiUrl; activeForm.method = 'POST'; + activeForm.fields = activeForm.fields.map((field) => { + if (field.name === 'affiliations') { + field.primaryLocale = activeForm.primaryLocale; + field.locales = activeForm.supportedFormLocales; + } + return field; + }); this.activeForm = activeForm; this.activeFormTitle = this.t('grid.action.addContributor'); const {openSideModal} = useModal(); @@ -396,6 +403,11 @@ export default { field.isVerified = author['orcidIsVerified'] ?? false; field.orcidVerificationRequested = author['orcidVerificationRequested']; + } else if (field.name === 'affiliations') { + field.authorId = author['id']; + field.primaryLocale = activeForm.primaryLocale; + field.locales = activeForm.supportedFormLocales; + field.value = author[field.name]; } else if (Object.keys(author).includes(field.name)) { field.value = author[field.name]; } diff --git a/src/components/Modal/SideModalBody.vue b/src/components/Modal/SideModalBody.vue index 917413984..1d0fa3677 100644 --- a/src/components/Modal/SideModalBody.vue +++ b/src/components/Modal/SideModalBody.vue @@ -46,7 +46,10 @@ -
+ +