Verified Commit 8f4cc8e5 authored by Sandro Lutz's avatar Sandro Lutz
Browse files

Add Studydocument upload form

parent c37b0aa8
import React, { useState, useRef, useCallback, useEffect } from 'react'
import PropTypes from 'prop-types'
import CloseIcon from '@material-ui/icons/Close'
import { makeStyles } from '@material-ui/core/styles'
import {
TextField,
Button,
IconButton,
Paper,
Popper,
MenuItem,
Typography,
MenuList,
} from '@material-ui/core'
const useStyles = makeStyles(
theme => ({
root: {
display: 'flex',
},
clearIndicator: {
marginRight: -2,
padding: 4,
color: theme.palette.action.active,
visibility: 'hidden',
},
clearIndicatorDirty: {
visibility: 'visible',
},
option: {
minHeight: 48,
display: 'flex',
justifyContent: 'flex-start',
alignItems: 'center',
cursor: 'pointer',
paddingTop: 6,
boxSizing: 'border-box',
outline: '0',
WebkitTapHighlightColor: 'transparent',
paddingBottom: 6,
paddingLeft: 16,
paddingRight: 16,
[theme.breakpoints.up('sm')]: {
minHeight: 'auto',
},
'&[aria-selected="true"]': {
backgroundColor: theme.palette.action.selected,
},
'&[data-focus="true"]': {
backgroundColor: theme.palette.action.hover,
},
'&[aria-disabled="true"]': {
opacity: 0.5,
pointerEvents: 'none',
},
'&:active': {
backgroundColor: theme.palette.action.selected,
},
},
textfield: {
width: 'inherit',
},
input: {
flex: 1,
},
button: {
marginLeft: theme.spacing(1),
},
popper: {
zIndex: theme.zIndex.modal,
},
menuList: {
maxHeight: '50vh',
overflowY: 'auto',
},
}),
{ name: 'selectTextField' }
)
const SelectTextField = ({
id: idProp,
label,
value,
getOptionLabel,
createEntryText,
createEntryTextShort,
disabled: disabledProp,
options,
onChange,
onOpen,
onClose,
clearText,
'aria-label': ariaLabel,
className,
...props
}) => {
const [createNewEntry, setCreateNewEntry] = useState(false)
const [inputValue, setInputValue] = useState(value)
const [open, setOpen] = useState(false)
const [anchorElement, setAnchorElement] = useState(null)
const firstFocus = useRef(true)
const inputRef = useRef(null)
const classes = useStyles()
const [defaultId, setDefaultId] = React.useState()
const id = idProp || defaultId
useEffect(() => {
// Fallback to this default id when possible.
// Use the random value for client-side rendering only.
// We can't use it server-side.
setDefaultId(`select-textfield-${Math.round(Math.random() * 1e5)}`)
}, [])
useEffect(() => {
setInputValue(value)
}, [value])
const filteredOptions = useCallback(() => {
if (!inputValue) {
return options
}
const regex = RegExp(`.*(${inputValue}).*`, 'gi')
return options.filter(item => regex.test(item))
}, [inputValue])
const handleOpen = () => {
if (open) {
return
}
if (onOpen) {
onOpen()
}
setOpen(true)
}
const handleClose = () => {
if (!open) {
return
}
if (onClose) {
onClose()
}
setOpen(false)
setInputValue(value)
}
// Focus the input when first interacting with the combobox
const handleRootClick = () => {
if (
firstFocus.current &&
inputRef.current.selectionEnd - inputRef.current.selectionStart === 0
) {
inputRef.current.focus()
}
firstFocus.current = false
}
const handleMouseDown = event => {
if (event.target.getAttribute('id') !== id) {
event.preventDefault()
}
}
const handleItemSelected = option => {
onChange({ target: { value: option } })
setCreateNewEntry(false)
handleClose()
}
const handleInputChange = event => {
const newValue = event.target.value
setInputValue(newValue)
if (createNewEntry) {
onChange({ target: { value: newValue || null } })
}
// automatically open list again if it was closed in the meantime
handleOpen()
}
const handleClear = () => {
setInputValue(null)
onChange({ target: { value: null } })
}
const handleCreateNewEntryClicked = () => {
setCreateNewEntry(true)
onChange({ target: { value: inputValue || null } })
}
const disabled = disabledProp || !!(value && inputValue && !createNewEntry)
const dirty = inputValue && inputValue.length > 0
const renderListOption = (option, index) => {
return (
<MenuItem
key={index}
onClick={() => {
handleItemSelected(option)
}}
>
<Typography variant="inherit">{getOptionLabel(option)}</Typography>
</MenuItem>
)
}
return (
<div
className={classes.root}
onMouseDown={handleMouseDown}
onClick={handleRootClick}
{...props}
>
<TextField
id={id}
label={label}
aria-label={ariaLabel}
value={inputValue || ''}
disabled={disabled}
onFocus={handleOpen}
onBlur={handleClose}
onChange={handleInputChange}
className={classes.textfield}
InputProps={{
ref: setAnchorElement,
className: classes.inputRoot,
// Disable browser's suggestion that might overlap with the popup.
// Handle autocomplete but not autofill.
autoComplete: 'nope',
autoCapitalize: 'none',
spellCheck: 'false',
endAdornment: (
<div className={classes.endAdornment}>
{createNewEntry && (
<Button disabled>{createEntryTextShort}</Button>
)}
<IconButton
aria-label={clearText}
title={clearText}
className={[
classes.clearIndicator,
dirty ? classes.clearIndicatorDirty : null,
].join(' ')}
onClick={handleClear}
>
<CloseIcon />
</IconButton>
</div>
),
}}
inputProps={{
ref: inputRef,
className: classes.input,
disabled,
}}
/>
{open &&
anchorElement &&
(!createNewEntry || filteredOptions().length > 0) ? (
<Popper
className={classes.popper}
style={{
width: anchorElement ? anchorElement.clientWidth : null,
}}
role="presentation"
anchorEl={anchorElement}
open
>
<Paper className={classes.paper}>
<MenuList className={classes.menuList}>
{filteredOptions().map((option, index) =>
renderListOption(option, index)
)}
{!createNewEntry && (
<MenuItem onClick={handleCreateNewEntryClicked}>
<Typography variant="inherit">{createEntryText}</Typography>
</MenuItem>
)}
</MenuList>
</Paper>
</Popper>
) : null}
</div>
)
}
SelectTextField.propTypes = {
/** @ignore */
className: PropTypes.string,
/** Used as the field id. Uses a randomly generated one if not set. */
id: PropTypes.string,
/** Label text for the text field. */
label: PropTypes.string,
/** If `true`, the input will be disabled. */
disabled: PropTypes.bool,
/** Array of options. */
options: PropTypes.array.isRequired,
/** Callback when input text has changed */
onChange: PropTypes.func,
/** Callback when the autocomplete list will be opened. */
onOpen: PropTypes.func,
/** Callback when the autocomplete list will be closed. */
onClose: PropTypes.func,
/** The value of the field. */
value: PropTypes.string,
/** Label used to improve accessibility. */
'aria-label': PropTypes.string,
/** Used to determine the string value for a given option. */
getOptionLabel: PropTypes.func,
/** Override the default text for the *clear* icon button. */
clearText: PropTypes.string,
/**
* Override the default text for the *create entry* hint rendered
* next to the input field.
*/
createEntryText: PropTypes.string,
/** Override the default text for the *create entry* menu item. */
createEntryTextShort: PropTypes.string,
}
SelectTextField.defaultProps = {
onChange: () => {},
getOptionLabel: x => x,
clearText: 'clear',
createEntryText: 'Create a new entry',
createEntryTextShort: 'New entry',
}
export default SelectTextField
......@@ -33,14 +33,12 @@ const TranslatedContent = ({ content, parseMarkdown, noEscape, ...props }) => {
return (
<div {...props}>
{/* <React.Fragment> */}
{hint}
<Typography
dangerouslySetInnerHTML={{
__html: parseMarkdown && message ? marked(escape(message)) : message,
__html: parseMarkdown && message ? marked(message) : message,
}}
/>
{/* </React.Fragment> */}
</div>
)
}
......
......@@ -20,45 +20,6 @@ const useStyles = makeStyles(
alignItems: 'center',
width: '100%',
},
notification: {
width: 'calc(100% - 2em)',
margin: '1em',
},
notificationColored: {
color: theme.palette.secondary.main,
},
additionalFields: {
transition: 'width 1s ease',
'& > *': {
width: '100%',
margin: '.5em 0',
},
},
additionalFieldsVisible: {
width: '350px',
padding: '1em',
position: 'relative',
'&:after': {
content: '" "',
display: 'block',
position: 'absolute',
backgroundColor: theme.palette.common.grey,
width: '4px',
top: '1em',
bottom: 0,
right: 0,
borderRadius: '2px',
overflow: 'hidden',
[theme.breakpoints.down('md')]: {
width: 'calc(100% - 2em)',
height: '4px',
margin: '0 1em',
},
},
},
description: {
flexGrow: 1,
padding: '1em',
......@@ -67,9 +28,6 @@ const useStyles = makeStyles(
width: '100%',
},
},
divider: {
gridArea: 'divider',
},
toolbar: {
width: '100%',
padding: '0 .4em',
......
......@@ -53,7 +53,7 @@ const JobofferSummary = ({ jobofferId, ...props }) => {
const intl = useIntl()
if (!joboffer || !joboffer.data) {
// TODO: ....
// TODO: Adapt skeleton for this layout
return (
<div className={classes.root} {...props}>
<Img className={classes.image} ratioX={6} ratioY={1} />
......
......@@ -125,6 +125,14 @@ const StudydocumentsFilter = ({ debounceTime, ...props }) => {
<Button name="reset" type="reset">
<FormattedMessage id="reset" />
</Button>
<Button
name="upload"
variant="contained"
color="primary"
onClick={() => navigate('/studydocuments/new')}
>
<FormattedMessage id="studydocuments.upload" />
</Button>
</FilterView>
)
}
......
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import { loadItem } from '../../store/common/actions'
import { STUDYDOCUMENTS } from '../../store/studydocuments/constants'
import Layout from '../layout'
import Spinner from '../general/spinner'
const useStyles = makeStyles(
{
root: {
textAlign: 'center',
paddingTop: '2em',
},
},
{ name: 'studydocumentsEdit' }
)
const StudydocumentForm = ({ studydocumentId }) => {
const [isLoaded, setIsLoaded] = useState(!studydocumentId)
const [values, setValues] = useState({})
// Selector for selected item (loaded from path)
const studydocument = useSelector(
state => state.studydocuments.items[studydocumentId]
)
const dispatch = useDispatch()
const classes = useStyles()
const theme = useTheme()
// Load document if not loaded yet.
useEffect(() => {
if (!studydocumentId) return
// Load studydocument if not already loaded.
if (!studydocument) {
dispatch(loadItem(STUDYDOCUMENTS, { id: studydocumentId }))
}
}, [studydocumentId])
useEffect(() => {
if (!isLoaded && studydocument && !studydocument.isPending) {
if (studydocument.data) {
setValues(studydocument.data)
}
setIsLoaded(true)
}
}, [studydocument])
return <div>This is great!</div>
}
StudydocumentForm.propTypes = {
/** studydocument id, if available */
studydocumentId: PropTypes.string,
}
export default StudydocumentForm
import React from 'react'
import PropTypes from 'prop-types'
import { useIntl } from 'gatsby-plugin-intl'
import { TextField } from '@material-ui/core'
const StudydocumentFormCourseYearField = ({ value, onChange, ...props }) => {
const intl = useIntl()
return (
<TextField
name="course_year"
label={intl.formatMessage({ id: 'studydocuments.courseYear' })}
value={value || ''}
type="number"
onChange={e => {
const { value: newValue } = e.target
onChange({
name: 'course_year',
value: newValue,
isValid: true,
})
}}
{...props}
/>
)
}
StudydocumentFormCourseYearField.propTypes = {
/** Value of the field. */
value: PropTypes.number,
/** Callback when the field value has changed. */
onChange: PropTypes.func.isRequired,
}
export default StudydocumentFormCourseYearField
import React from 'react'
import PropTypes from 'prop-types'
import { useIntl, FormattedMessage } from 'gatsby-plugin-intl'
import { FormControl, InputLabel, Select } from '@material-ui/core'
const DEPARTMENTS = [
'itet',
'mavt',
'arch',
'baug',
'bsse',
'infk',
'matl',
'biol',
'chab',
'math',
'phys',
'erdw',
'usys',
'hest',
'mtec',
'gess',
]
const StudydocumentFormDepartmentField = ({ value, onChange, ...props }) => {
const intl = useIntl()
return (
<FormControl {...props}>
<InputLabel>
<FormattedMessage id="studydocuments.department" />
</InputLabel>
<Select
native
name="department"
value={value || ''}
onChange={e => {
const { value: newValue } = e.target
onChange({
name: 'department',
value: newValue,
isValid: true,
})
}}
inputProps={{
'aria-label': intl.formatMessage({
id: 'studydocuments.department',
}),
}}
>
<option value=""></option>
{DEPARTMENTS.map(department => (
<option key={department} value={department}>
{`D-${department.toUpperCase()}`}
</option>
))}
</Select>
</FormControl>
)
}
StudydocumentFormDepartmentField.propTypes = {
/** Value of the field. */
value: PropTypes.string,
/** Callback when the field value has changed. */
onChange: PropTypes.func.isRequired,
}