From 4d87b72db0a04d65f0bca2ca9e967bbb195bbfa9 Mon Sep 17 00:00:00 2001 From: "Dusan Mijatovic (PC2020)" Date: Mon, 28 Aug 2023 16:33:35 +0200 Subject: [PATCH] refactor: basic announcement setup extract api calls from ui components for future test purposes and use theme colors. --- database/022-create-maintenance-tables.sql | 4 +- .../components/Announcement/Announcement.tsx | 31 ++-- frontend/components/admin/AdminNav.tsx | 13 +- .../admin/announcements/AnnouncementsForm.tsx | 163 ++++++++---------- .../admin/announcements/apiAnnouncement.tsx | 82 +++++++++ .../admin/announcements/getAnnouncement.tsx | 20 --- .../components/admin/announcements/index.tsx | 86 ++------- .../admin/announcements/useAnnouncement.tsx | 33 ++++ frontend/config/getSettingsServerSide.ts | 19 +- frontend/config/rsdSettingsReducer.ts | 9 +- frontend/pages/_app.tsx | 10 +- frontend/pages/admin/announcements.tsx | 7 +- frontend/pages/admin/orcid-users.tsx | 5 +- frontend/public/data/settings.json | 4 +- 14 files changed, 252 insertions(+), 234 deletions(-) create mode 100644 frontend/components/admin/announcements/apiAnnouncement.tsx delete mode 100644 frontend/components/admin/announcements/getAnnouncement.tsx create mode 100644 frontend/components/admin/announcements/useAnnouncement.tsx diff --git a/database/022-create-maintenance-tables.sql b/database/022-create-maintenance-tables.sql index a6812a1be..eaf1c3d59 100644 --- a/database/022-create-maintenance-tables.sql +++ b/database/022-create-maintenance-tables.sql @@ -1,5 +1,7 @@ -- SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +-- SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -- SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +-- SPDX-FileCopyrightText: 2023 Netherlands eScience Center -- -- SPDX-License-Identifier: Apache-2.0 -- SPDX-License-Identifier: EUPL-1.2 @@ -38,6 +40,6 @@ CREATE TRIGGER sanitise_update_global_announcement BEFORE UPDATE ON global_annou ALTER TABLE global_announcement ENABLE ROW LEVEL SECURITY; -CREATE POLICY anyone_can_read ON global_announcement FOR SELECT TO rsd_web_anon, rsd_user USING (TRUE);´ +CREATE POLICY anyone_can_read ON global_announcement FOR SELECT TO rsd_web_anon, rsd_user USING (TRUE); CREATE POLICY admin_all_rights ON global_announcement TO rsd_admin USING (TRUE) WITH CHECK (TRUE); diff --git a/frontend/components/Announcement/Announcement.tsx b/frontend/components/Announcement/Announcement.tsx index 7a9b7d2c7..eef9d0d87 100644 --- a/frontend/components/Announcement/Announcement.tsx +++ b/frontend/components/Announcement/Announcement.tsx @@ -1,36 +1,37 @@ // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: EUPL-1.2 -import Button from '@mui/material/Button' +import {useState} from 'react' +import IconButton from '@mui/material/IconButton' import CancelIcon from '@mui/icons-material/Cancel' import ErrorIcon from '@mui/icons-material/Error' -import {useState} from 'react' export default function Announcement({announcement}: {announcement: string | null}) { const [open, setOpen] = useState(true) - if (announcement === null || announcement === undefined) return null - if (!open) return null + // do not show if no content or close icon is clicked + if (typeof(announcement) == 'undefined' || announcement === null || open===false) return null return (
-
- -
-
- {announcement} -
- + +
) } diff --git a/frontend/components/admin/AdminNav.tsx b/frontend/components/admin/AdminNav.tsx index 78b827c9f..afe1d6971 100644 --- a/frontend/components/admin/AdminNav.tsx +++ b/frontend/components/admin/AdminNav.tsx @@ -21,14 +21,9 @@ import SpellcheckIcon from '@mui/icons-material/Spellcheck' import DomainAddIcon from '@mui/icons-material/DomainAdd' import AccountCircleIcon from '@mui/icons-material/AccountCircle' import FluorescentIcon from '@mui/icons-material/Fluorescent' +import CampaignIcon from '@mui/icons-material/Campaign' export const adminPages = { - notifications: { - title: 'Announcements', - subtitle: 'Show global notifications to all users', - icon: , - path: '/admin/announcements', - }, pages:{ title: 'Public pages', subtitle: 'Manage markdown pages', @@ -71,6 +66,12 @@ export const adminPages = { icon: , path: '/admin/keywords', }, + announcements: { + title: 'Announcement', + subtitle: 'Notification to all users', + icon: , + path: '/admin/announcements', + } } // extract page types from the object diff --git a/frontend/components/admin/announcements/AnnouncementsForm.tsx b/frontend/components/admin/announcements/AnnouncementsForm.tsx index 625f5880d..2ded76825 100644 --- a/frontend/components/admin/announcements/AnnouncementsForm.tsx +++ b/frontend/components/admin/announcements/AnnouncementsForm.tsx @@ -1,127 +1,102 @@ // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: EUPL-1.2 -import Button from '@mui/material/Button' -import AddIcon from '@mui/icons-material/Add' - +import {useForm} from 'react-hook-form' import {useSession} from '~/auth' -import {Controller, SubmitHandler, UseControllerProps, useController, useForm} from 'react-hook-form' -import AutosaveControlledTextField, {OnSaveProps} from '~/components/form/AutosaveControlledTextField' -import {Input, Switch} from '@mui/material' +import useSnackbar from '~/components/snackbar/useSnackbar' import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' -import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' -import {GetServerSidePropsContext} from 'next' -import logger from '~/utils/logger' -import {useEffect, useState} from 'react' import ControlledSwitch from '~/components/form/ControlledSwitch' import ControlledTextField from '~/components/form/ControlledTextField' +import {AnnouncementItem, saveAnnouncement} from './apiAnnouncement' const formId = 'announcements-form' -type EditAnnouncementItem = { - [id: string]: any, - text: string | null, - enabled: boolean -} - -type EditAnnouncementFormProps = { - data: EditAnnouncementItem -} - -export default function AnnouncementsForm({data}: EditAnnouncementFormProps) { +export default function AnnouncementsForm({data}: { data: AnnouncementItem|null }) { const {token} = useSession() - const {handleSubmit, register, control, setValue, formState: {errors}} = useForm({ + const {showErrorMessage} = useSnackbar() + const {handleSubmit, register, control, reset, formState} = useForm({ defaultValues: { - ...data + ...data }, mode: 'onChange' }) - function onSubmit(dataToSubmit: EditAnnouncementItem) { - saveAnnouncement(dataToSubmit).then( - (resp) => { - if (resp && [200, 201].includes(resp.status) && resp?.object) { - const newObj = resp.object[0] - setValue('enabled', newObj.enabled) - setValue('id', newObj.id) - setValue('text', newObj.text) - } else { - console.log('Error saving data:', resp) - } - } - ) - } + // track form state + const {isValid, isDirty} = formState - async function saveAnnouncement(item: EditAnnouncementItem) { - try { - let method - let url - if (item.id == '') { - delete item.id - method = 'POST' - url = '/api/v1/global_announcement' - } else { - method = 'PATCH' - url = `/api/v1/global_announcement?id=eq.${item.id}` - } - const resp = await fetch(url, { - method: method, - headers: { - ...createJsonHeaders(token), - Prefer: 'return=representation', - }, - body: JSON.stringify(item) - }) - if ([200, 201].includes(resp.status)) { - return { - status: 201, - object: await resp.json() - } - } - // return extractReturnMessage(resp, item.text ?? '') - } catch (e: any) { - logger(`saveAnnouncement: ${e?.message}`, 'error') - return { - status: 500, - message: e?.message + async function onSubmit(item: AnnouncementItem) { + const resp = await saveAnnouncement({ + id: item.id, + enabled: item.enabled, + text: item.text + }, token) + + if (resp.status === 200) { + // use values returned from api + const update = { + id: resp.message?.id ?? null, + enabled: resp.message.enabled ?? false, + text: resp.message.text ?? null } + // will reset form state + reset(update) + } else { + showErrorMessage(`Failed to save announcement. ${resp.message}`) } } + function isSaveDisabled() { + if (isValid === false) return true + if (isDirty === false) return true + return false + } + return (
- - - - + className="flex-1" + > + {/* id */} + + {/* active/visible */} + +
+ + +
) } diff --git a/frontend/components/admin/announcements/apiAnnouncement.tsx b/frontend/components/admin/announcements/apiAnnouncement.tsx new file mode 100644 index 000000000..d45a46b1e --- /dev/null +++ b/frontend/components/admin/announcements/apiAnnouncement.tsx @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: EUPL-1.2 + +import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' +import logger from '~/utils/logger' + +export type NewAnnouncement = { + enabled: boolean + text: string | null +} + +export type AnnouncementItem = NewAnnouncement & { + id: string +} + +export async function getAnnouncement(token?:string) { + try { + const url = getBaseUrl() + '/global_announcement?select=id,enabled,text' + const resp = await fetch(url, { + method: 'GET', + headers: { + ...createJsonHeaders(token), + } + }) + + if (resp.status === 200) { + const json = await resp.json() + if (json.length > 0) { + return json[0] as AnnouncementItem + } + return null + } + // unexpected return status + logger(`getAnnouncement: ${resp?.status}-${resp.statusText}`,'warn') + return null + } catch (e: any) { + logger(`getAnnouncement: ${e?.message}`, 'error') + return null + } +} + +export async function saveAnnouncement(item: AnnouncementItem, token: string) { + try { + let url = `${getBaseUrl()}/global_announcement` + let method = 'POST' + if (item.id) { + url += `?id=eq.${item.id}` + method = 'PATCH' + } + const resp = await fetch(url, { + method, + headers: { + ...createJsonHeaders(token), + Prefer: 'return=representation', + }, + body: JSON.stringify({ + enabled: item.enabled, + text: item.text + }) + }) + if ([200, 201, 204].includes(resp.status)) { + const json = await resp.json() + return { + status: 200, + message: json[0] + } + } + logger(`saveAnnouncement: ${resp?.status}-${resp.statusText}`, 'warn') + return extractReturnMessage(resp, item.text ?? '') + } catch (e: any) { + logger(`saveAnnouncement: ${e?.message}`, 'error') + return { + status: 500, + message: e?.message + } + } +} diff --git a/frontend/components/admin/announcements/getAnnouncement.tsx b/frontend/components/admin/announcements/getAnnouncement.tsx deleted file mode 100644 index 2827c548b..000000000 --- a/frontend/components/admin/announcements/getAnnouncement.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) -// SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences -// -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: EUPL-1.2 - -import {getBaseUrl} from '~/utils/fetchHelpers' -import logger from '~/utils/logger' - -export default async function getAnnouncement() { - const url = getBaseUrl() + '/global_announcement' - const resp = await fetch(url, {method: 'GET',}) - const json = await resp.json() - if (json[0] && json[0].enabled) { - return json[0].text - } else { - logger('getAnnouncement failed:') - return null - } -} diff --git a/frontend/components/admin/announcements/index.tsx b/frontend/components/admin/announcements/index.tsx index e550bd704..fa75e757d 100644 --- a/frontend/components/admin/announcements/index.tsx +++ b/frontend/components/admin/announcements/index.tsx @@ -1,92 +1,30 @@ // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: EUPL-1.2 -import Button from '@mui/material/Button' -import AddIcon from '@mui/icons-material/Add' - -import {useSession} from '~/auth' -import {SubmitHandler, UseControllerProps, useController, useForm} from 'react-hook-form' -import AutosaveControlledTextField, {OnSaveProps} from '~/components/form/AutosaveControlledTextField' -import {Input, Switch} from '@mui/material' -import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener' -import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers' -import {GetServerSidePropsContext} from 'next' -import logger from '~/utils/logger' -import {useEffect, useState} from 'react' import AnnouncementsForm from './AnnouncementsForm' import ContentLoader from '~/components/layout/ContentLoader' - -const formId = 'announcements-form' - -type EditAnnouncementItem = { - [id: string]: any, - text: string | null, - enabled: boolean -} - - -function getData({token}: {token: string}) { - const [loading, setLoading] = useState(true) - const [item, setItem] = useState() - - async function getAnnouncement() { - const url = getBaseUrl() + '/global_announcement' - setLoading(true) - const resp = await fetch(url, { - method: 'GET', - headers: { - ...createJsonHeaders(token), - } - }) - - if (resp.status === 200) { - const json = await resp.json() - if (json[0]) { - setItem({ - id: json[0].id, - enabled: json[0].enabled, - text: json[0].text - }) - } else { - setItem({ - enabled: false, - text: '' - }) - } - setLoading(false) - } - } - - useEffect(() => { - getAnnouncement() - }, []) - - return { - formProps: item, - loading - } - -} - +import useAnnouncement from './useAnnouncement' export default function AnnouncementsPage() { - const {token} = useSession() - const {formProps, loading} = getData({token}) + const {loading,announcement} = useAnnouncement() + + // console.group('AnnouncementsPage') + // console.log('loading...', loading) + // console.log('announcement...', announcement) + // console.groupEnd() if (loading) return ( ) - if (formProps) { - return ( - - ) - } - - return + return ( + + ) } diff --git a/frontend/components/admin/announcements/useAnnouncement.tsx b/frontend/components/admin/announcements/useAnnouncement.tsx new file mode 100644 index 000000000..a8bc58cfe --- /dev/null +++ b/frontend/components/admin/announcements/useAnnouncement.tsx @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// +// SPDX-License-Identifier: Apache-2.0 + +import {useEffect, useState} from 'react' +import {AnnouncementItem, getAnnouncement} from './apiAnnouncement' +import {useSession} from '~/auth' + + +export default function useAnnouncement() { + const {token} = useSession() + const [loading, setLoading] = useState(true) + const [announcement, setAnnouncement] = useState(null) + + useEffect(() => { + let abort = false + + getAnnouncement(token).then(item => { + if (abort) return + setAnnouncement(item) + setLoading(false) + }) + + return ()=>{abort=true} + }, [token]) + + return { + announcement, + loading + } + +} diff --git a/frontend/config/getSettingsServerSide.ts b/frontend/config/getSettingsServerSide.ts index 5566bd43a..146875e2f 100644 --- a/frontend/config/getSettingsServerSide.ts +++ b/frontend/config/getSettingsServerSide.ts @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: EUPL-1.2 @@ -13,7 +15,7 @@ import logger from '~/utils/logger' import {getPageLinks} from '~/components/admin/pages/useMarkdownPages' import {defaultRsdSettings, RsdSettingsState} from './rsdSettingsReducer' import defaultSettings from '~/config/defaultSettings.json' -import getAnnouncement from '~/components/admin/announcements/getAnnouncement' +import {getAnnouncement} from '~/components/admin/announcements/apiAnnouncement' /** * getThemeSettings from local json file @@ -44,14 +46,14 @@ export async function getRsdSettings() { export async function getSettingsServerSide(req: IncomingMessage | undefined, query: ParsedUrlQuery): Promise { // if not SSR we return default if (typeof req === 'undefined') return defaultRsdSettings as RsdSettingsState - // get links - const pages = await getPageLinks({is_published: true}) - // get announcments - const announcement = await getAnnouncement() // extract embed flag const embed = typeof query?.embed !== 'undefined' - // get settings (host and theme) - const settings = await getRsdSettings() + // get links, settings and announcements in parallel + const [pages, settings, announcement] = await Promise.all([ + getPageLinks({is_published: true}), + getRsdSettings(), + getAnnouncement() + ]) // compose all settings const rsdSettings = { ...defaultRsdSettings, @@ -60,11 +62,10 @@ export async function getSettingsServerSide(req: IncomingMessage | undefined, qu theme: settings.theme, pages, embed, - announcement: announcement + announcement: announcement?.enabled ? announcement?.text : undefined } // console.group('getSettingsServerSide') // console.log('rsdSettings...', rsdSettings) // console.groupEnd() - return rsdSettings } diff --git a/frontend/config/rsdSettingsReducer.ts b/frontend/config/rsdSettingsReducer.ts index 134dcfc58..6eb49d03a 100644 --- a/frontend/config/rsdSettingsReducer.ts +++ b/frontend/config/rsdSettingsReducer.ts @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: EUPL-1.2 @@ -13,10 +15,10 @@ import defaultSettings from '~/config/defaultSettings.json' export type RsdSettingsState = { host: RsdHost, embed: boolean + theme: RsdTheme, links?: CustomLink[] pages?: RsdLink[] - theme: RsdTheme, - announcement: string | null + announcement?: string | null } export type RsdHost = { @@ -70,8 +72,7 @@ export const defaultRsdSettings: RsdSettingsState = { }, embed: false, theme: defaultSettings.theme, - links:[], - announcement: null + links:[] } export function rsdSettingsReducer(state: RsdSettingsState, action: RsdSettingsAction) { diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx index b5a36ee61..3f1c0a1a0 100644 --- a/frontend/pages/_app.tsx +++ b/frontend/pages/_app.tsx @@ -1,8 +1,9 @@ // SPDX-FileCopyrightText: 2021 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2021 - 2023 dv4all +// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Jesús García Gonzalez (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences // // SPDX-License-Identifier: Apache-2.0 @@ -71,9 +72,9 @@ Router.events.on('routeChangeError', ()=>{ function RsdApp(props: MuiAppProps) { const { Component, emotionCache = clientSideEmotionCache, - pageProps, session, settings, matomo + pageProps, session, settings, matomo, } = props - const announcement = settings.announcement + //currently we support only default (light) and dark RSD theme for MUI // const muiTheme = loadMuiTheme(settings.theme.mode as RsdThemes) const router = useRouter() @@ -133,7 +134,8 @@ function RsdApp(props: MuiAppProps) { {/* Matomo cookie consent dialog */} - + {/* RSD admin announcements/ system notifications */} + ) diff --git a/frontend/pages/admin/announcements.tsx b/frontend/pages/admin/announcements.tsx index 02f05478f..366cdcb2f 100644 --- a/frontend/pages/admin/announcements.tsx +++ b/frontend/pages/admin/announcements.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences +// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -14,8 +16,7 @@ import AdminPageWithNav from '~/components/admin/AdminPageWithNav' import {adminPages} from '~/components/admin/AdminNav' import AnnouncementsPage from '~/components/admin/announcements/index' - -const pageTitle = `${adminPages['keywords'].title} | Admin page | ${app.title}` +const pageTitle = `${adminPages['announcements'].title} | Admin page | ${app.title}` export default function AdminAnnouncementsPage(props:any) { @@ -24,7 +25,7 @@ export default function AdminAnnouncementsPage(props:any) { {pageTitle} - + diff --git a/frontend/pages/admin/orcid-users.tsx b/frontend/pages/admin/orcid-users.tsx index 420602374..5851ce537 100644 --- a/frontend/pages/admin/orcid-users.tsx +++ b/frontend/pages/admin/orcid-users.tsx @@ -1,5 +1,6 @@ +// SPDX-FileCopyrightText: 2022 - 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2022 Ewan Cahen (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2022 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2023 dv4all // @@ -68,7 +69,7 @@ export default function OrcidWitelistPage() {
-
+