Skip to content

Commit

Permalink
feat: mentions overview for admins
Browse files Browse the repository at this point in the history
  • Loading branch information
ewan-escience committed May 15, 2024
1 parent d6200a9 commit 0ce8b52
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 20 deletions.
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) <e.cahen@esciencecenter.nl>
//
// 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
74 changes: 74 additions & 0 deletions frontend/components/admin/mentions-overview/MentionsOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

import {useEffect, useState} from 'react'
import MentionsOverviewList from '~/components/admin/mentions-overview/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)
}, [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>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: 2024 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// 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>
}>
<MentionViewItem item={mention} pos={page * rows + idx + 1}></MentionViewItem>
</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) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) <christian.meessen@gfz-potsdam.de>
// 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
}
}
31 changes: 29 additions & 2 deletions frontend/components/mention/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 - 2023 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 - 2024 Ewan Cahen (Netherlands eScience Center) <e.cahen@esciencecenter.nl>
// 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',
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
Loading

0 comments on commit 0ce8b52

Please sign in to comment.