Skip to content

Commit

Permalink
Merge pull request #1197 from research-software-directory/1194-mentio…
Browse files Browse the repository at this point in the history
…ns-overview-admins

1194 mentions overview admins
  • Loading branch information
ewan-escience authored May 17, 2024
2 parents d6200a9 + 4634584 commit 05baed1
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 26 deletions.
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion documentation/docs/03-rsd-instance/03-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center)
SPDX-FileCopyrightText: 2023 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center
SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
SPDX-FileCopyrightText: 2023 Ewan Cahen (Netherlands eScience Center) <[email protected]>
SPDX-FileCopyrightText: 2023 dv4all
SPDX-FileCopyrightText: 2024 Christian Meeßen (GFZ) <[email protected]>
SPDX-FileCopyrightText: 2024 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
Expand Down
20 changes: 17 additions & 3 deletions frontend/components/admin/AdminNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) <[email protected]>
//
// 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'
Expand All @@ -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'

Expand Down Expand Up @@ -69,6 +72,12 @@ export const adminPages = {
icon: <SpellcheckIcon />,
path: '/admin/keywords',
},
mentions: {
title: 'Mentions',
subtitle: '',
icon: <ReceiptLongIcon />,
path: '/admin/mentions',
},
logs:{
title: 'Error logs',
subtitle: '',
Expand All @@ -80,7 +89,7 @@ export const adminPages = {
subtitle: '',
icon: <CampaignIcon />,
path: '/admin/announcements',
}
},
}

// extract page types from the object
Expand All @@ -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'
}
}}
>
<ListItemIcon>
{item.icon}
Expand Down
76 changes: 76 additions & 0 deletions frontend/components/admin/mentions/MentionsOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// 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 (
<section className="flex-1">
<div className="flex flex-wrap items-center justify-end">
<Searchbox/>
<Pagination/>
</div>
<div>
<MentionsOverviewList list={mentionList} onUpdate={() => fetchAndSetMentionList(sanitisedSearch)}></MentionsOverviewList>
</div>
</section>
)
}
84 changes: 84 additions & 0 deletions frontend/components/admin/mentions/MentionsOverviewList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// 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<boolean>(false)
const [mentionToEdit, setMentionToEdit] = useState<MentionItemProps | undefined>(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>
{list.map((mention, idx) => {
return (
<ListItem
key={mention.id}
secondaryAction={
<IconButton onClick={() => {setModalOpen(true); setMentionToEdit(mention)}} ><EditIcon></EditIcon></IconButton>
}
className="hover:bg-base-200 flex-1 pr-4"
>
<MentionViewItem item={mention} pos={page * rows + idx + 1} />
</ListItem>
)
})}
</List>
<EditMentionModal
title={mentionToEdit?.id as string ?? 'undefined'}
open={modalOpen}
pos={undefined} //why does this exist?
item={mentionToEdit}
onCancel={() => setModalOpen(false)}
onSubmit={({data}) => {updateMention(data)}}
/>
</>
)
};

60 changes: 49 additions & 11 deletions frontend/components/mention/EditMentionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <[email protected]>
// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) <[email protected]>
// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
Expand All @@ -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,
Expand All @@ -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<MentionItemProps>({
mode: 'onChange',
Expand All @@ -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
Expand All @@ -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"
>
<DialogTitle sx={{
Expand Down Expand Up @@ -130,6 +134,23 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t
width: ['100%'],
padding: '1rem 1.5rem'
}}>
{isAdmin &&
<>
<ControlledTextField
control={control}
options={{
name: 'doi',
label: config.doi.label,
useNull: true,
defaultValue: formData?.doi,
helperTextMessage: config.doi.help,
helperTextCnt: `${formData?.doi?.length || 0}/${config.doi.validation.maxLength.value}`,
}}
rules={config.doi.validation}
/>
<div className="py-2"></div>
</>
}
<ControlledTextField
control={control}
options={{
Expand Down Expand Up @@ -261,9 +282,28 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t
}}
rules={config.note.validation}
/>
<Alert severity="warning" sx={{marginTop: '1rem'}}>
The information can not be edited after creation.
</Alert>
{isAdmin &&
<>
<div className="py-2"></div>
<ControlledTextField
control={control}
options={{
name: 'external_id',
label: config.external_id.label,
useNull: true,
defaultValue: formData?.external_id,
helperTextMessage: config.external_id.help,
helperTextCnt: `${formData?.external_id?.length || 0}/${config.external_id.validation.maxLength.value}`,
}}
rules={config.external_id.validation}
/>
<div className="py-2"></div>
</>
}
{!isAdmin &&
<Alert severity="warning" sx={{marginTop: '1rem'}}>
The information can not be edited after creation.
</Alert>}
</DialogContent>
<DialogActions sx={{
padding: '1rem 1.5rem',
Expand All @@ -274,7 +314,7 @@ export default function EditMentionModal({open, onCancel, onSubmit, item, pos, t
tabIndex={1}
onClick={handleCancel}
color="secondary"
sx={{marginRight:'2rem'}}
sx={{marginRight: '2rem'}}
>
Cancel
</Button>
Expand All @@ -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
}
}
Loading

0 comments on commit 05baed1

Please sign in to comment.