Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit software and project improvements #558

Merged
merged 8 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading