-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: basic announcement setup extract api calls from ui componen…
…ts for future test purposes and use theme colors.
- Loading branch information
1 parent
decff62
commit 4d87b72
Showing
14 changed files
with
252 additions
and
234 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
@@ -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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 69 additions & 94 deletions
163
frontend/components/admin/announcements/AnnouncementsForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
82
frontend/components/admin/announcements/apiAnnouncement.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
20
frontend/components/admin/announcements/getAnnouncement.tsx
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.