Skip to content

Commit

Permalink
feat: edit contributor avatar, roles and affiliation using aggregated…
Browse files Browse the repository at this point in the history
… values from other RSD entries

feat: edit team member avatar, roles and affiliation using aggregated values from other RSD entries
refactor: move methods into hooks file
refactor: use person shared components for modal and find person
  • Loading branch information
dmijatovic committed Sep 16, 2024
1 parent 1954262 commit 2d49aa5
Show file tree
Hide file tree
Showing 37 changed files with 1,443 additions and 2,160 deletions.
29 changes: 27 additions & 2 deletions database/104-person-views.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
-- SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center)
-- SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center
-- SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
-- SPDX-FileCopyrightText: 2023 Netherlands eScience Center
-- SPDX-FileCopyrightText: 2023 dv4all
--
-- SPDX-License-Identifier: Apache-2.0
Expand Down Expand Up @@ -101,3 +101,28 @@ INNER JOIN
LEFT JOIN
public_profile() ON public_profile.orcid = team_member.orcid
$$;

--ROLES ALREADY IN RSD
--Use this to suggest roles in the modal
CREATE FUNCTION suggested_roles() RETURNS
VARCHAR[] LANGUAGE sql STABLE AS
$$
SELECT
ARRAY_AGG("role")
FROM (
SELECT
"role"
FROM
contributor
WHERE
"role" IS NOT NULL
UNION
SELECT
"role"
FROM
team_member
WHERE
"role" IS NOT NULL
) roles
;
$$;
24 changes: 17 additions & 7 deletions frontend/components/form/ControlledAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ type ControlledAutocompleteProps = {
name: string,
label: string,
options: string[],
helperTextMessage: string,
helperTextMessage?: string | JSX.Element,
control: any
rules: any
variant?: 'standard' | 'outlined' | 'filled'
}

export default function ControlledAutocomplete({
name, label, control, rules, options, variant, helperTextMessage}: ControlledAutocompleteProps) {
// const [open,setOpen]=useState(false)
name, label, control, rules, options, variant, helperTextMessage
}: ControlledAutocompleteProps) {

return (
<Controller
name={name}
Expand All @@ -40,10 +41,19 @@ export default function ControlledAutocomplete({
freeSolo={true}
multiple={false}
options={options}
onInputChange={(e, value) => {
// debugger
if (value === '') onChange(null)
onChange(value)
value={value}
onInputChange={(e, newVal) => {
// Save typed input into the controller (form data)
// Note! onChange triggers the dirty state
// we do not want to call it when data is not changed
if (newVal !== value){
// debugger
if (newVal === '') {
onChange(null)
}else{
onChange(newVal)
}
}
}}
onChange={(e, item, reason) => {
// debugger
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/form/ControlledTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function ControlledTextField<T>({options, control, rules}:Control
minRows={options?.maxRows ?? undefined}
maxRows={options?.maxRows ?? undefined}
rows={options?.rows ?? undefined}
error={error ? true: false}
error={error ? true : false}
label={options?.label ?? 'Label not provided'}
type={options?.type ?? 'text'}
fullWidth={options?.fullWidth ?? true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
// SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 - 2023 dv4all
// SPDX-FileCopyrightText: 2022 Christian Meeßen (GFZ) <christian.meessen@gfz-potsdam.de>
// SPDX-FileCopyrightText: 2022 Helmholtz Centre Potsdam - GFZ German Research Centre for Geosciences
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
// SPDX-FileCopyrightText: 2023 Netherlands eScience Center
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

Expand All @@ -17,112 +12,86 @@ import useMediaQuery from '@mui/material/useMediaQuery'

import {UseFormSetValue, UseFormWatch, useForm} from 'react-hook-form'

import {useSession} from '~/auth'
import {upsertImage} from '~/utils/editImage'
import {postContributor} from '~/utils/editContributors'
import {getPropsFromObject} from '~/utils/getPropsFromObject'
import {ContributorProps, SaveContributor} from '~/types/Contributor'
import useSnackbar from '~/components/snackbar/useSnackbar'
import {Person} from '~/types/Contributor'
import ControlledTextField from '~/components/form/ControlledTextField'
import ControlledSwitch from '~/components/form/ControlledSwitch'
import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener'
import ControlledAutocomplete from '~/components/form/ControlledAutocomplete'
import {AggregatedPerson} from '~/components/person/groupByOrcid'
import AvatarOptionsPerson, {RequiredAvatarProps} from '~/components/person/AvatarOptionsPerson'
import {contributorInformation as config} from '../editSoftwareConfig'
import useAggregatedPerson from './useAggregatedPerson'
import {modalConfig} from './config'

type AggregatedContributorModalProps = {
open: boolean,
type InputProps={
label:string
help?: string | JSX.Element
validation?: any
}

type AggregatedPersonModalConfig={
is_contact_person: Omit<InputProps,'validation'>,
given_names: InputProps,
family_names: InputProps,
email_address: InputProps,
orcid: InputProps,
role: InputProps,
affiliation: InputProps
}

type AggregatedPersonModalProps = {
onCancel: () => void,
onSubmit: (contributpr: SaveContributor) => void,
contributor: NewRsdContributor
onSubmit: (person: FormPerson) => void,
person: Person
// default title is Profile, provide other value
title?: string
// default labels and validation are defined in ./config
// optionally provide custom config
config?: AggregatedPersonModalConfig
}

export type NewRsdContributor = AggregatedPerson & {
software: string
selected_avatar: string | null
avatar_id: string|null
avatar_b64: string|null
avatar_mime_type: string|null
role: string|null
is_contact_person: boolean
position: number
export type FormPerson = Person & {
initial_avatar_id: string|null
}

const formId = 'aggregated-person-modal'

export default function AggregatedContributorModal({open, onCancel, onSubmit, contributor}: AggregatedContributorModalProps) {
const {token} = useSession()
const {showErrorMessage} = useSnackbar()
const smallScreen = useMediaQuery('(max-width:600px)')
const {handleSubmit, watch, formState, reset, control, register, setValue} = useForm<NewRsdContributor>({
export default function AggregatedPersonModal({
person,onCancel,onSubmit,
title='Profile',config=modalConfig
}: AggregatedPersonModalProps) {
const {loading, options} = useAggregatedPerson(person?.orcid)
const smallScreen = useMediaQuery('(max-width:640px)')
const {handleSubmit, watch, formState, control, register, setValue} = useForm<FormPerson>({
mode: 'onChange',
defaultValues: {
...contributor
...person,
// copy avatar id
initial_avatar_id: person.avatar_id
}
})

// extract
const {isValid, isDirty} = formState
const formData = watch()

// console.group('AggregatedContributorModal')
// console.group('AggregatedPersonModal')
// console.log('errors...', errors)
// console.log('isDirty...', isDirty)
// console.log('isValid...', isValid)
// console.log('formData...', formData)
// console.log('loading...', loading)
// console.log('options...', options)
// console.groupEnd()

function handleCancel(e?:any, reason?:'backdropClick' | 'escapeKeyDown') {
if (reason && reason==='backdropClick') return
// reset form
reset()
// hide
onCancel()
}

async function onSave(data: NewRsdContributor) {
// UPLOAD avatar
if (data.avatar_b64 && data.avatar_mime_type) {
// split base64 to use only encoded content
const b64data = data.avatar_b64.split(',')[1]
const upload = await upsertImage({
data: b64data,
mime_type: data.avatar_mime_type,
token
})

// debugger
if (upload.status === 201) {
// update data values
data.avatar_id = upload.message
} else {
showErrorMessage(`Failed to upload image. ${upload.message}`)
return
}
}
// prepare data object for save (remove helper props)
const contributor: SaveContributor = getPropsFromObject(data, ContributorProps)
// new team member we need to add
const resp = await postContributor({
contributor,
token
})
// debugger
if (resp.status === 201) {
// get id out of message
contributor.id = resp.message
// pass member to parent
onSubmit(contributor)
} else {
showErrorMessage(`Failed to add contributor. ${resp.message}`)
}
}

return (
<Dialog
// use fullScreen modal for small screens (< 600px)
fullScreen={smallScreen}
open={open}
open={true}
onClose={handleCancel}
>
<DialogTitle sx={{
Expand All @@ -132,36 +101,34 @@ export default function AggregatedContributorModal({open, onCancel, onSubmit, co
color: 'primary.main',
fontWeight: 500
}}>
Add contributor
{title}
</DialogTitle>
<form
id={formId}
onSubmit={handleSubmit((data) => onSave(data))}
onSubmit={handleSubmit((data) => onSubmit(data))}
autoComplete="off"
>
{/* hidden inputs */}
<input type="hidden"
{...register('software')}
/>
<input type="hidden"
{...register('position')}
/>
<input type="hidden"
{...register('avatar_b64')}
{...register('avatar_id')}
/>
<input type="hidden"
{...register('avatar_mime_type')}
{...register('initial_avatar_id')}
/>
<DialogContent sx={{
width: ['100%', '37rem'],
width: ['100%', '40rem'],
}}>
<AvatarOptionsPerson
watch={watch as unknown as UseFormWatch<RequiredAvatarProps>}
setValue={setValue as unknown as UseFormSetValue<RequiredAvatarProps>}
avatar_options={contributor.avatar_options}
avatar_options={options?.avatars ?? []}
loading={loading}
/>
<div className="py-2"/>
<section className="py-4 grid grid-cols-[1fr,1fr] gap-8">
<section className="py-4 grid grid-cols-[1fr,1fr] gap-4">
<ControlledTextField
control={control}
options={{
Expand All @@ -186,20 +153,24 @@ export default function AggregatedContributorModal({open, onCancel, onSubmit, co
}}
rules={config.family_names.validation}
/>
<ControlledAutocomplete
name="email_address"
label={config.email_address.label}
<ControlledTextField
options={{
name: 'email_address',
label: config.email_address.label,
useNull: true,
defaultValue: person?.email_address,
helperTextMessage: config.email_address.help,
helperTextCnt: `${formData?.email_address?.length || 0}/${config.email_address.validation().maxLength.value}`,
}}
control={control}
options={contributor.email_options}
helperTextMessage={config.email_address.help}
rules={config.email_address.validation}
rules={config.email_address.validation(formData.is_contact_person)}
/>
<ControlledTextField
options={{
name: 'orcid',
label: config.orcid.label,
useNull: true,
defaultValue: contributor?.orcid,
defaultValue: person?.orcid,
helperTextMessage: config.orcid.help,
// helperTextCnt: `${formData?.orcid?.length || 0}/${config.orcid.validation.maxLength.value}`,
}}
Expand All @@ -210,23 +181,23 @@ export default function AggregatedContributorModal({open, onCancel, onSubmit, co
name="role"
label={config.role.label}
control={control}
options={contributor.role_options}
options={options?.roles ?? []}
helperTextMessage={config.role.help}
rules={config.role.validation}
/>
<ControlledAutocomplete
name="affiliation"
label={config.affiliation.label}
options={contributor.affiliation_options}
options={options?.affiliations ?? []}
control={control}
rules={config.affiliation.validation}
helperTextMessage={config.affiliation.help}
rules={config.affiliation.validation}
/>
</section>
<section>
<ControlledSwitch
name="is_contact_person"
label="Contact person"
label={config.is_contact_person.label}
control={control}
defaultValue={false}
/>
Expand Down
Loading

0 comments on commit 2d49aa5

Please sign in to comment.