Skip to content

Commit

Permalink
refactor: basic announcement setup extract api calls from ui componen…
Browse files Browse the repository at this point in the history
…ts for future test purposes and use theme colors.
  • Loading branch information
dmijatovic committed Aug 28, 2023
1 parent decff62 commit 4d87b72
Show file tree
Hide file tree
Showing 14 changed files with 252 additions and 234 deletions.
4 changes: 3 additions & 1 deletion database/022-create-maintenance-tables.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
-- SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) <[email protected]>
-- 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
Expand Down Expand Up @@ -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);
31 changes: 16 additions & 15 deletions frontend/components/Announcement/Announcement.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) <[email protected]>
// 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 (
<div
className="fixed bottom-0 right-0 w-full bg-warning text-white font-extrabold flex"
className="flex justify-center items-center fixed bottom-0 right-0 w-full bg-warning text-warning-content text-xl px-4"
>
<div className='my-auto pl-5'>
<ErrorIcon className='align-middle' />
</div>
<div className="w-full p-5 inline-block">
<span className='align-middle'>{announcement}</span>
</div>
<Button
className='ml-auto'
<ErrorIcon/>
<span className='flex-1 py-8 ml-2'>{announcement}</span>
<IconButton
size='large'
onClick={() => {setOpen(false)}}
sx={{
color: 'warning.contrastText'
}}
>
<CancelIcon className='text-white' />
</Button>
<CancelIcon />
</IconButton>
</div>
)
}
13 changes: 7 additions & 6 deletions frontend/components/admin/AdminNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: <DescriptionIcon />,
path: '/admin/announcements',
},
pages:{
title: 'Public pages',
subtitle: 'Manage markdown pages',
Expand Down Expand Up @@ -71,6 +66,12 @@ export const adminPages = {
icon: <SpellcheckIcon />,
path: '/admin/keywords',
},
announcements: {
title: 'Announcement',
subtitle: 'Notification to all users',
icon: <CampaignIcon />,
path: '/admin/announcements',
}
}

// extract page types from the object
Expand Down
163 changes: 69 additions & 94 deletions frontend/components/admin/announcements/AnnouncementsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,127 +1,102 @@
// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) <[email protected]>
// 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<EditAnnouncementItem>({
const {showErrorMessage} = useSnackbar()
const {handleSubmit, register, control, reset, formState} = useForm<AnnouncementItem>({
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 (
<form
id={formId}
onSubmit={handleSubmit(onSubmit)}
className="w-full md:w-[42rem]"
>
<Input type="hidden" {...register('id')}/>
<ControlledTextField
control={control}
options={{
name: 'text',
label: 'Announcement'
}}
rules={{
maxLength: {
value: 300,
message: 'Maximum length is 300.'
}
}}
/>
<ControlledSwitch
label='Visible'
name='enabled'
control={control}
/>
<SubmitButtonWithListener
disabled={false}
formId={formId}
/>
className="flex-1"
>
{/* id */}
<input type="hidden" {...register('id')} />
{/* active/visible */}
<ControlledSwitch
label='Visible'
name='enabled'
control={control}
/>
<div className="flex justify-between items-center gap-8 py-4">
<ControlledTextField
control={control}
options={{
name: 'text',
label: 'Announcement',
multiline: true
}}
rules={{
required: 'Announcement text is required',
minLength: {
value: 3,
message: 'Minimum length is 3.'
},
maxLength: {
value: 300,
message: 'Maximum length is 300.'
}
}}
/>
<SubmitButtonWithListener
disabled={isSaveDisabled()}
formId={formId}
/>
</div>
</form>
)
}
82 changes: 82 additions & 0 deletions frontend/components/admin/announcements/apiAnnouncement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2023 Christian Meeßen (GFZ) <[email protected]>
// 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
}
}
}
20 changes: 0 additions & 20 deletions frontend/components/admin/announcements/getAnnouncement.tsx

This file was deleted.

Loading

0 comments on commit 4d87b72

Please sign in to comment.