Skip to content

Commit

Permalink
Merge pull request #558 from research-software-directory/autosave-pro…
Browse files Browse the repository at this point in the history
…ject-info

Edit software and project improvements
  • Loading branch information
dmijatovic authored Oct 10, 2022
2 parents c40eaf8 + 5101169 commit ed8c958
Show file tree
Hide file tree
Showing 100 changed files with 3,696 additions and 3,506 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ services:
# dockerfile to use for build
dockerfile: Dockerfile
# update version number to correspond to frontend/package.json
image: rsd/frontend:1.6.1
image: rsd/frontend:1.6.2
environment:
# it uses values from .env file
- POSTGREST_URL
Expand Down
64 changes: 64 additions & 0 deletions frontend/components/form/AutosaveControlledTextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2022 dv4all
//
// SPDX-License-Identifier: Apache-2.0

import {useController} from 'react-hook-form'
import ControlledTextField, {ControlledTextFieldOptions} from '~/components/form/ControlledTextField'

export type OnSaveProps = {
name: string,
value: string
}

export type AutosaveControlledTextField = {
control: any
options: ControlledTextFieldOptions
rules?: any
onSaveField: ({name,value}: OnSaveProps) => void
}

export default function AutosaveControlledTextField({control,options,rules,onSaveField}:AutosaveControlledTextField) {
const {field:{value},fieldState:{isDirty,error}} = useController({
control,
name: options.name
})

// add onBlur fn to muiProps
if (options.muiProps) {
options.muiProps['onBlur'] = onSaveInfo
} else {
// create muiProps
options['muiProps'] = {
onBlur: onSaveInfo
}
}

/**
* This function is passed to onBlur event of ControlledTextField component.
* We use value from react-hook-form controller because the Controlled component
* will pass a null value rather than an empty string when isNull prop is provided.
*/
function onSaveInfo() {
if (isDirty === false) return
if (error) return
// console.group('AutosaveControlledTextField')
// console.log('onSaveInfo...', options.name)
// console.log('value...', value)
// console.log('isDirty...', isDirty)
// console.groupEnd()
// call provided save fn
onSaveField({
name: options.name,
value
})
}

return (
<ControlledTextField
options={options}
control={control}
rules={rules}
/>
)
}
5 changes: 4 additions & 1 deletion frontend/components/form/ControlledSwitch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ type ControlledSwitchProps = {
control: any,
defaultValue?: boolean
disabled?: boolean
onSave?:(value:boolean)=>void
}


export default function ControlledSwitch({label, name, defaultValue = false, control, disabled=false}:ControlledSwitchProps) {
export default function ControlledSwitch({label, name,
defaultValue = false, control, disabled = false, onSave}: ControlledSwitchProps) {
// console.log('ControlledSwitch.defaultValue...', defaultValue)
return (
<Controller
Expand All @@ -33,6 +35,7 @@ export default function ControlledSwitch({label, name, defaultValue = false, con
checked={value}
onChange={({target}) => {
onChange(target.checked)
if(onSave) onSave(target.checked)
}}
disabled={disabled ?? false}
/>
Expand Down
14 changes: 10 additions & 4 deletions frontend/components/form/ControlledTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import {useEffect, useRef} from 'react'
import {Controller} from 'react-hook-form'
import TextField from '@mui/material/TextField'
import TextField, {TextFieldProps} from '@mui/material/TextField'
import HelperTextWithCounter from './HelperTextWithCounter'

export type ControlledTextFieldOptions = {
Expand All @@ -26,11 +26,16 @@ export type ControlledTextFieldOptions = {
helperTextMessage?: string | JSX.Element
helperTextCnt?: string
disabled?: boolean
muiProps?: TextFieldProps
}

export default function ControlledTextField({options, control, rules}: {
options: ControlledTextFieldOptions, control: any, rules:any
}) {
export type ControlledTextFieldProps = {
options: ControlledTextFieldOptions,
control: any,
rules?: any
}

export default function ControlledTextField({options, control, rules}:ControlledTextFieldProps) {
const inputRef = useRef<HTMLInputElement | null>(null)

useEffect(() => {
Expand Down Expand Up @@ -92,6 +97,7 @@ export default function ControlledTextField({options, control, rules}: {
onChange(target.value)
}
}}
{...options.muiProps}
/>
)
}}
Expand Down
11 changes: 4 additions & 7 deletions frontend/components/form/MarkdownInputWithPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: Apache-2.0

import {useState, useEffect} from 'react'
import {useState, useEffect, FocusEventHandler} from 'react'
import Tabs from '@mui/material/Tabs'
import Tab from '@mui/material/Tab'

Expand All @@ -18,11 +18,11 @@ type MarkdownInputWithPreviewProps = {
length: number
maxLength: number
}
onBlur?:(e:FocusEventHandler<HTMLTextAreaElement>)=>void
}


export default function MarkdownInputWithPreview({markdown, register, disabled = true,
autofocus = false, helperInfo}:MarkdownInputWithPreviewProps) {
autofocus = false, helperInfo, onBlur}:MarkdownInputWithPreviewProps) {
const [tab, setTab] = useState(0)

useEffect(() => {
Expand Down Expand Up @@ -120,11 +120,8 @@ export default function MarkdownInputWithPreview({markdown, register, disabled =
id="markdown-textarea"
rows={20}
className="text-base-content w-full h-full pt-4 px-8 font-mono text-sm"
// onInput={({target}:{target:any}) => {
// target.style.height = ''
// target.style.height = target.scrollHeight + 'px'
// }}
{...register}
onBlur={onBlur}
></textarea>
</div>
<div
Expand Down
6 changes: 3 additions & 3 deletions frontend/components/layout/SortableList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export default function SortableList<T extends RequiredProps>({
useSensor(MouseSensor,{
// required to enable click events
// on draggable items with buttons
activationConstraint: {
distance: 8,
}
// activationConstraint: {
// distance: 8,
// }
})
)

Expand Down
23 changes: 12 additions & 11 deletions frontend/components/projects/edit/EditProjectNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,34 @@ import ListItemIcon from '@mui/material/ListItemIcon'
import ListItemText from '@mui/material/ListItemText'

import {app} from '~/config/app'
import useOnUnsaveChange from '~/utils/useOnUnsavedChange'
import {editProjectSteps} from './editProjectSteps'
import useProjectContext from './useProjectContext'

export default function EditProjectNav() {
const {formState:{isDirty,isValid},reset} = useFormContext()
const {formState:{isDirty,isValid,dirtyFields}} = useFormContext()
const {step, setEditStep} = useProjectContext()

// watch for unsaved changes on page reload
useOnUnsaveChange({
isDirty,
isValid,
warning: app.unsavedChangesMessage
})

function onChangeStep(pos: number) {
const newStep = editProjectSteps[pos]
// ignore click on same step
if (newStep.label===step?.label) return
// if unsaved changes in the form when changing step
if (isDirty === true) {
// isDirty prop can be incorrect when defaultValue
// was undefined at form initalization.
// see https://github.com/react-hook-form/react-hook-form/issues/6105
// To mitigate this we include dirtyFields object as additional check
if (isDirty === true && Object.keys(dirtyFields).length > 0) {
// console.group('EditProjectNav')
// console.log('isDirty...', isDirty)
// console.log('isValid...', isValid)
// console.log('dirtyFields...', dirtyFields)
// console.groupEnd()
// notify user about unsaved changes
const leavePage = confirm(app.unsavedChangesMessage)
// if user is OK to leave section without saving
if (leavePage === true) {
// clean form
reset()
// reset()
// change step
setEditStep(editProjectSteps[pos])
}
Expand Down
46 changes: 20 additions & 26 deletions frontend/components/projects/edit/EditProjectStickyHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,34 @@ import {useState, useRef} from 'react'
import {useRouter} from 'next/router'
import Button from '@mui/material/Button'

import {useFormContext} from 'react-hook-form'
import {useController, useFormContext} from 'react-hook-form'

import StickyHeader from '../../layout/StickyHeader'
import useStickyHeaderBorder from '~/components/layout/useStickyHeaderBorder'
import useProjectContext from './useProjectContext'
import SubmitButtonWithListener from '~/components/form/SubmitButtonWithListener'

export default function EditProjectStickyHeader() {
const {project, step} = useProjectContext()
const {project} = useProjectContext()
const router = useRouter()
const {formState:{isValid,isDirty}} = useFormContext()
const {control} = useFormContext()
const {field:{value:slug},fieldState:{error:slugError}} = useController({
name: 'slug',
control
})
const headerRef = useRef(null)
const [classes, setClasses] = useState('')
// add border when header is at the top of the page
const {el} = useStickyHeaderBorder({
headerRef, setClasses
})

function isSaveDisabled() {
if (isDirty === false || isValid === false) {
return true
}
return false
}

// console.group('EditProjectStickyHeader')
// console.log('isDirty...', isDirty)
// console.log('isValid...', isValid)
// console.log('errors...', errors)
// console.groupEnd()
// if (isDirty) {
// console.group('EditProjectStickyHeader')
// console.log('isDirty...', isDirty)
// console.log('isValid...', isValid)
// console.log('dirtyFields...', dirtyFields)
// console.groupEnd()
// }

return (
<StickyHeader className={`md:flex py-4 w-full bg-white ${classes}`}>
Expand All @@ -53,22 +51,18 @@ export default function EditProjectStickyHeader() {
type="button"
color="secondary"
onClick={() => {
const slug = router.query['slug']
// const slug = router.query['slug']
router.push(`/projects/${slug}`)
// complete page reload?
// location.href=`/projects/${slug}`
}}
sx={{
marginRight:'2rem'
marginRight:'0.5rem'
}}
disabled={typeof slugError !=='undefined'}
>
VIEW
VIEW PAGE
</Button>
{step?.formId ?
<SubmitButtonWithListener
formId={step?.formId}
disabled={isSaveDisabled()}
/>
: null
}
</div>
</StickyHeader>
)
Expand Down
18 changes: 18 additions & 0 deletions frontend/components/projects/edit/editProjectReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import logger from '~/utils/logger'
import {EditProjectState} from './editProjectContext'

export enum EditProjectActionType {
SET_PROJECT_TITLE = 'SET_PROJECT_TITLE',
SET_PROJECT_SLUG = 'SET_PROJECT_SLUG',
SET_PROJECT_INFO = 'SET_PROJECT_INFO',
SET_EDIT_STEP = 'SET_EDIT_STEP',
SET_LOADING = 'SET_LOADING'
Expand Down Expand Up @@ -39,6 +41,22 @@ export function editProjectReducer(state: EditProjectState, action: Action) {
...action.payload
}
}
case EditProjectActionType.SET_PROJECT_SLUG:
return {
...state,
project: {
...state.project,
slug: action.payload
}
}
case EditProjectActionType.SET_PROJECT_TITLE:
return {
...state,
project: {
...state.project,
title: action.payload
}
}
case EditProjectActionType.SET_LOADING: {
return {
...state,
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/projects/edit/editProjectSteps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export type EditProjectStep = {

export const editProjectSteps: EditProjectStep[] = [
{
formId: 'project-information',
// formId: 'project-information',
label: 'Information',
icon: <InfoIcon />,
component: (props?) => <ProjectInformation {...props} />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logger from '~/utils/logger'
import {createJsonHeaders, extractReturnMessage, getBaseUrl} from '~/utils/fetchHelpers'
import {MentionItemProps} from '~/types/Mention'
import {addMentionItem} from '~/utils/editMentions'
import {addOrGetMentionItem} from '~/utils/editMentions'

export async function findPublicationByTitle({project, searchFor, token}:
{ project: string, searchFor: string, token: string }) {
Expand Down Expand Up @@ -35,11 +35,11 @@ export async function addImpactItem({item, project, token}: { item: MentionItemP
// new item not in rsd
if (item.id === null) {
// add mention item to RSD
const resp = await addMentionItem({
const resp = await addOrGetMentionItem({
mention: item,
token
})
if (resp.status !== 201) {
if (resp.status !== 200) {
// exit
return {
status: resp.status,
Expand Down
Loading

0 comments on commit ed8c958

Please sign in to comment.