diff --git a/docker-compose.yml b/docker-compose.yml index da3eeecd6..6a9e22076 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -110,7 +110,7 @@ services: # dockerfile to use for build dockerfile: Dockerfile # update version number to correspond to frontend/package.json - image: rsd/frontend:2.8.3 + image: rsd/frontend:2.8.4 environment: # it uses values from .env file - POSTGREST_URL @@ -203,7 +203,7 @@ services: context: ./documentation # dockerfile to use for build dockerfile: Dockerfile - image: rsd/documentation:2.0.0 + image: rsd/documentation:2.0.1 expose: - "80" networks: diff --git a/documentation/docs/03-rsd-instance/03-administration.md b/documentation/docs/03-rsd-instance/03-administration.md index 88e2d498e..d1b8822a1 100644 --- a/documentation/docs/03-rsd-instance/03-administration.md +++ b/documentation/docs/03-rsd-instance/03-administration.md @@ -121,7 +121,7 @@ Additionally to organisation maintainers, administrators can edit the following * Parent organisation: if the organisation belongs to another organisation, add the ID of the parent organisation here. The organisation ID is displayed in the Admin section of the respective organisation. :::warning -**Note** that changing the parent organisation will also affect the path under which the organisation is accessible. The path is determined by the organisation hirarchy, e.g. `/organisations/parent-organisation/child-organisation`. +**Note** that changing the parent organisation will also affect the path under which the organisation is accessible. The path is determined by the organisation hierarchy, e.g. `/organisations/parent-organisation/child-organisation`. ::: The settings are visible in the *Admin section* under the *General settings* tab of the organisation settings: @@ -139,6 +139,14 @@ You can delete the keyword only when it is not used in any software or project. ![animation](img/admin-keywords.gif) +## Mentions + +In this section, admins can search for mentions and edit them. If you enter a DOI or UUID, we search on that field only. Otherwise, we search on title, authors, journal, URL, note and external ID (like an OpenAlex ID). + +:::warning +Edit mentions with care: they might be referenced to in multiple places. If you want to fully change a mention attached to e.g. a software page, you should delete it there and create a new one instead of editing it. +::: + ## Error logs This section shows any errors originating from the background processes like data scrapers. Provided information should be understandable to rsd administrators in the most cases. The error object contains error response. The stacktrace is convenient for the programmers. The link will navigate you to the software or the project that triggers the error. diff --git a/documentation/docs/03-rsd-instance/03-administration.md.license b/documentation/docs/03-rsd-instance/03-administration.md.license index f2d9b7bae..d2d297c2f 100644 --- a/documentation/docs/03-rsd-instance/03-administration.md.license +++ b/documentation/docs/03-rsd-instance/03-administration.md.license @@ -1,7 +1,7 @@ SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all) -SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) SPDX-FileCopyrightText: 2023 dv4all SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences diff --git a/frontend/components/admin/AdminNav.tsx b/frontend/components/admin/AdminNav.tsx index 2dafc0610..fa2ea2ed5 100644 --- a/frontend/components/admin/AdminNav.tsx +++ b/frontend/components/admin/AdminNav.tsx @@ -4,10 +4,12 @@ // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // SPDX-FileCopyrightText: 2023 dv4all +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 import {useRouter} from 'next/router' +import Link from 'next/link' import List from '@mui/material/List' import ListItemButton from '@mui/material/ListItemButton' @@ -23,6 +25,7 @@ import AccountCircleIcon from '@mui/icons-material/AccountCircle' import FluorescentIcon from '@mui/icons-material/Fluorescent' import CampaignIcon from '@mui/icons-material/Campaign' import BugReportIcon from '@mui/icons-material/BugReport' +import ReceiptLongIcon from '@mui/icons-material/ReceiptLong' import {editMenuItemButtonSx} from '~/config/menuItems' @@ -69,6 +72,12 @@ export const adminPages = { icon: , path: '/admin/keywords', }, + mentions: { + title: 'Mentions', + subtitle: '', + icon: , + path: '/admin/mentions', + }, logs:{ title: 'Error logs', subtitle: '', @@ -80,7 +89,7 @@ export const adminPages = { subtitle: '', icon: , path: '/admin/announcements', - } + }, } // extract page types from the object @@ -104,8 +113,13 @@ export default function AdminNav() { data-testid="admin-nav-item" key={`step-${pos}`} selected={item.path === router.route} - onClick={() => router.push(item.path)} - sx={editMenuItemButtonSx} + href = {item.path} + component = {Link} + sx={{...editMenuItemButtonSx, + ':hover': { + color: 'text.primary' + } + }} > {item.icon} diff --git a/frontend/components/admin/mentions/MentionsOverview.tsx b/frontend/components/admin/mentions/MentionsOverview.tsx new file mode 100644 index 000000000..d34e92bf3 --- /dev/null +++ b/frontend/components/admin/mentions/MentionsOverview.tsx @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useEffect, useState} from 'react' +import MentionsOverviewList from '~/components/admin/mentions/MentionsOverviewList' +import {extractSearchTerm, SearchTermInfo} from '~/components/software/edit/mentions/utils' +import Searchbox from '~/components/search/Searchbox' +import Pagination from '~/components/pagination/Pagination' +import usePaginationWithSearch from '~/utils/usePaginationWithSearch' +import {extractCountFromHeader} from '~/utils/extractCountFromHeader' +import {paginationUrlParams} from '~/utils/postgrestUrl' + +const uuidRegex = /^\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$/ + +export default function MentionsOverview() { + const [mentionList, setMentionList] = useState([]) + const {searchFor, page, rows, setCount} = usePaginationWithSearch('Find mentions') + + const sanitisedSearch = sanitiseSearch(searchFor) + useEffect(() => { + fetchAndSetMentionList(sanitisedSearch) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sanitisedSearch, page, rows]) + + function fetchAndSetMentionList(sanitisedSearch: undefined | string): void { + fetch(`/api/v1/mention?${createSearchQueryParameters(sanitisedSearch)}&${paginationUrlParams({rows, page})}`, { + headers: { + 'Prefer': 'count=exact' + } + }) + .then(res => { + setCount(extractCountFromHeader(res.headers) ?? 0) + return res.json() + }) + .then(arr => setMentionList(arr)) + } + + function createSearchQueryParameters(sanitisedSearch: undefined | string): string { + if (sanitisedSearch === undefined) { + return '' + } + + if (uuidRegex.test(sanitisedSearch.trim())) { + return `id=eq.${sanitisedSearch.trim().toLowerCase()}` + } + + const searchTypeTerm: SearchTermInfo = extractSearchTerm(sanitisedSearch) + const termEscaped = encodeURIComponent(sanitisedSearch) + if (searchTypeTerm.type === 'doi') { + return `doi=eq.${termEscaped}` + } + return `or=(title.ilike.*${termEscaped}*,authors.ilike.*${termEscaped}*,journal.ilike.*${termEscaped}*,url.ilike.*${termEscaped}*,note.ilike.*${termEscaped}*,external_id.ilike.*${termEscaped}*)` + } + + function sanitiseSearch(search: string): string | undefined { + if (!search || search.length < 3 ) { + return undefined + } + return search + } + + return ( +
+
+ + +
+
+ fetchAndSetMentionList(sanitisedSearch)}> +
+
+ ) +} diff --git a/frontend/components/admin/mentions/MentionsOverviewList.tsx b/frontend/components/admin/mentions/MentionsOverviewList.tsx new file mode 100644 index 000000000..18961ae03 --- /dev/null +++ b/frontend/components/admin/mentions/MentionsOverviewList.tsx @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2024 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useState} from 'react' +import List from '@mui/material/List' +import ListItem from '@mui/material/ListItem' +import EditIcon from '@mui/icons-material/Edit' +import IconButton from '@mui/material/IconButton' +import {MentionItemProps} from '~/types/Mention' +import MentionViewItem from '~/components/mention/MentionViewItem' +import EditMentionModal from '~/components/mention/EditMentionModal' +import {createJsonHeaders} from '~/utils/fetchHelpers' +import {useSession} from '~/auth' +import useSnackbar from '~/components/snackbar/useSnackbar' +import usePaginationWithSearch from '~/utils/usePaginationWithSearch' + +function leaveOutSomeFieldsReplacer(key: string, value: any) { + if (key === 'id' || key === 'doi_registration_date' || key === 'created_at' || key === 'updated_at') { + return undefined + } else { + return value + } +} + +export default function MentionsOverviewList({list, onUpdate}: { list: MentionItemProps[], onUpdate: Function }) { + const [modalOpen, setModalOpen] = useState(false) + const [mentionToEdit, setMentionToEdit] = useState(undefined) + const {token} = useSession() + const {showErrorMessage} = useSnackbar() + const {page, rows} = usePaginationWithSearch('Find mentions') + + async function updateMention(data: MentionItemProps) { + const id = data.id as string + const body = JSON.stringify(data, leaveOutSomeFieldsReplacer) + const resp = await fetch(`/api/v1/mention?id=eq.${id}`, { + method: 'PATCH', + body: body, + headers: createJsonHeaders(token) + }) + if (resp.ok) { + setModalOpen(false) + onUpdate() + } else { + showErrorMessage(`got status ${resp.status}:${await resp.text()}`) + } + } + + if (list.length === 0) { + return 'No mentions to show' + + } + + return ( + <> + + {list.map((mention, idx) => { + return ( + {setModalOpen(true); setMentionToEdit(mention)}} > + } + className="hover:bg-base-200 flex-1 pr-4" + > + + + ) + })} + + setModalOpen(false)} + onSubmit={({data}) => {updateMention(data)}} + /> + + ) +}; + diff --git a/frontend/components/mention/EditMentionModal.tsx b/frontend/components/mention/EditMentionModal.tsx index d7ae49884..360c240f4 100644 --- a/frontend/components/mention/EditMentionModal.tsx +++ b/frontend/components/mention/EditMentionModal.tsx @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) // SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) // SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences @@ -25,6 +25,7 @@ import {mentionModal as config, mentionType} from './config' import {MentionItemProps, MentionTypeKeys} from '../../types/Mention' import ControlledSelect from '~/components/form/ControlledSelect' import SubmitButtonWithListener from '../form/SubmitButtonWithListener' +import {useSession} from '~/auth' export type EditMentionModalProps = { open: boolean, @@ -48,9 +49,12 @@ const mentionTypeOptions = manualOptions.map(key => { } }) -const formId='edit-mention-form' +const formId = 'edit-mention-form' export default function EditMentionModal({open, onCancel, onSubmit, item, pos, title}: EditMentionModalProps) { + const {user} = useSession() + const isAdmin = user?.role === 'rsd_admin' + const smallScreen = useMediaQuery('(max-width:600px)') const {handleSubmit, watch, formState, reset, control, register} = useForm({ mode: 'onChange', @@ -76,7 +80,7 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t } }, [item, reset]) - function handleCancel(reason:any) { + function handleCancel(reason: any) { if (reason === 'backdropClick') { // we do not cancel on backdrop click // only on escape or using cancel button @@ -101,7 +105,7 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t // use fullScreen modal for small screens (< 600px) fullScreen={smallScreen} open={open} - onClose={(e,reason)=>handleCancel(reason)} + onClose={(e, reason) => handleCancel(reason)} maxWidth="md" > + {isAdmin && + <> + +
+ + } - - The information can not be edited after creation. - + {isAdmin && + <> +
+ +
+ + } + {!isAdmin && + + The information can not be edited after creation. + } Cancel @@ -288,8 +328,6 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t ) function isSaveDisabled() { - if (isValid === false) return true - if (isDirty === false) return true - return false + return !isValid || !isDirty } } diff --git a/frontend/components/mention/MentionViewItem.tsx b/frontend/components/mention/MentionViewItem.tsx index 7a1abc8e7..98fcd9dbf 100644 --- a/frontend/components/mention/MentionViewItem.tsx +++ b/frontend/components/mention/MentionViewItem.tsx @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) // // SPDX-License-Identifier: Apache-2.0 @@ -54,7 +55,13 @@ export default function MentionViewItem({item, pos}: {item: MentionItemProps, po if (item?.url) { return ( - + {renderItemBody()} ) diff --git a/frontend/components/mention/config.ts b/frontend/components/mention/config.ts index 42e9588ae..312b710bc 100644 --- a/frontend/components/mention/config.ts +++ b/frontend/components/mention/config.ts @@ -1,11 +1,12 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 - 2023 dv4all +// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 import {MentionByType, MentionTypeKeys} from '~/types/Mention' +import {doiRegexStrict} from '~/components/software/edit/mentions/utils' export const findMention={ // title: 'Add publication', @@ -20,6 +21,21 @@ export const findMention={ export const mentionModal = { sectionTitle: 'Mentions', + doi: { + label: 'DOI', + help: undefined, + validation: { + required: false, + maxLength: { + value: 255, + message: 'Maximum length is 255' + }, + pattern: { + value: doiRegexStrict, + message: 'The DOI should look like 10.XXX/XXX' + } + } + }, title: { label: 'Title *', help: 'Publication title is required', @@ -111,6 +127,17 @@ export const mentionModal = { } } }, + external_id: { + label: 'External ID', + help: 'An ID used by e.g. OpenAlex', + validation: { + required: false, + maxLength: { + value: 500, + message: 'Maximum length is 500' + } + } + }, image_url: { label: 'Image url*', help: 'Url to publication image is required for highlighted mention', diff --git a/frontend/components/software/edit/mentions/utils.ts b/frontend/components/software/edit/mentions/utils.ts index c33f970cc..629e8e48a 100644 --- a/frontend/components/software/edit/mentions/utils.ts +++ b/frontend/components/software/edit/mentions/utils.ts @@ -1,12 +1,13 @@ +// SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 const doiRegex = /10(\.\w+)+\/\S+/ +export const doiRegexStrict = /^10(\.\w+)+\/\S+$/ export type SearchTermInfo = { term: string, diff --git a/frontend/pages/admin/mentions.tsx b/frontend/pages/admin/mentions.tsx new file mode 100644 index 000000000..2e19d0541 --- /dev/null +++ b/frontend/pages/admin/mentions.tsx @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 dv4all +// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) +// +// SPDX-License-Identifier: Apache-2.0 + +import Head from 'next/head' + +import {app} from '../../config/app' +import DefaultLayout from '~/components/layout/DefaultLayout' +import AdminPageWithNav from '~/components/admin/AdminPageWithNav' +import {adminPages} from '~/components/admin/AdminNav' +import MentionsOverview from '~/components/admin/mentions/MentionsOverview' +import {SearchProvider} from '~/components/search/SearchContext' +import {PaginationProvider} from '~/components/pagination/PaginationContext' + +const pageTitle = `${adminPages['mentions'].title} | Admin page | ${app.title}` + +const pagination = { + count: 0, + page: 0, + rows: 12, + rowsOptions: [12, 24, 48], + labelRowsPerPage: 'Per page' +} + +export default function MentionsOverviewPage(props: any) { + return ( + + + {pageTitle} + + + + + + + + + + ) +} diff --git a/frontend/types/Mention.ts b/frontend/types/Mention.ts index 99b2bfd7f..0ff05f214 100644 --- a/frontend/types/Mention.ts +++ b/frontend/types/Mention.ts @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 dv4all // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) @@ -36,6 +36,7 @@ export type MentionItemProps = { mention_type: MentionTypeKeys | null source: string note?: string | null + external_id?: string | null } export const mentionColumns ='id,doi,url,title,authors,publisher,publication_year,journal,page,image_url,mention_type,source,note'