Commit b91bc30d authored by David Sikter's avatar David Sikter
Browse files

Merge branch '833-quantity-type-url-link' into 'develop'

Resolve "Quantity type url/link"

Closes #833

See merge request !713
parents ace60545 e84704e5
Pipeline #138291 failed with stages
in 74 minutes and 35 seconds
......@@ -5,7 +5,7 @@ import { useEntryPageContext } from '../entry/EntryPageContext'
import _ from 'lodash'
import ListEditQuantity from '../editQuantity/ListEditQuantity'
import {DateTimeEditQuantity} from '../editQuantity/DateTimeEditQuantity'
import {StringEditQuantity} from '../editQuantity/StringEditQuantity'
import { StringEditQuantity, URLEditQuantity } from '../editQuantity/StringEditQuantity'
import {NumberEditQuantity} from '../editQuantity/NumberEditQuantity'
import {EnumEditQuantity} from '../editQuantity/EnumEditQuantity'
import {AutocompleteEditQuantity} from '../editQuantity/AutocompleteEditQuantity'
......@@ -19,6 +19,7 @@ import { RadioEnumEditQuantity } from '../editQuantity/RadioEnumEditQuantity'
const editQuantityComponents = {
NumberEditQuantity: NumberEditQuantity,
StringEditQuantity: StringEditQuantity,
URLEditQuantity: URLEditQuantity,
EnumEditQuantity: EnumEditQuantity,
SelectEnumEditQuantity: EnumEditQuantity,
RadioEnumEditQuantity: RadioEnumEditQuantity,
......
......@@ -19,7 +19,7 @@ import React, {useRef, useState} from 'react'
import PropTypes from 'prop-types'
import {Card, Box, Typography, Grid, CardContent} from '@material-ui/core'
import {NumberEditQuantity} from './NumberEditQuantity'
import {StringEditQuantity} from './StringEditQuantity'
import {StringEditQuantity, URLEditQuantity} from './StringEditQuantity'
import {EnumEditQuantity} from './EnumEditQuantity'
import {AutocompleteEditQuantity} from './AutocompleteEditQuantity'
import {RadioEnumEditQuantity} from './RadioEnumEditQuantity'
......@@ -116,6 +116,18 @@ export function EditQuantityExamples() {
<StringEditQuantity {...createDefaultProps('string')} />
</Example>
</Grid>
<Grid item>
<Example
code={`
url_link:
type: string
m_annotations:
eln:
component: URLEditQuantity`}
>
<URLEditQuantity {...createDefaultProps('url')}/>
</Example>
</Grid>
<Grid item>
<Example
code={`
......
......@@ -99,4 +99,19 @@ test('correctly renders edit quantities', async () => {
await waitFor(() => expect(numberFieldValueInputInMeter.value).toEqual('1.5'))
await waitFor(() => expect(numberFieldUnitInputInMeter.value).toEqual('Å'))
await waitFor(() => expect(screen.queryByText(/"float_with_bounds": 1\.5e-10/i)).toBeInTheDocument())
// Test for the URLEditQuantity
const UrlComponent = screen.getByTestId('URLEditQuantity')
const invalidUrlMsg = () => within(UrlComponent).queryByText(/invalid url string!/i)
const UrlTextbox = () => within(UrlComponent).getByRole('textbox')
const redirectButton = () => within(UrlComponent).queryByRole('button', { name: /open_link/i })
expect(invalidUrlMsg()).not.toBeInTheDocument()
expect(redirectButton()).not.toBeInTheDocument()
fireEvent.change(UrlTextbox(), { target: { value: 'a' } })
await waitFor(() => expect(invalidUrlMsg()).toBeInTheDocument())
fireEvent.change(UrlTextbox(), { target: { value: 'https://nomad-lab.eu/' } })
await waitFor(() => expect(invalidUrlMsg()).not.toBeInTheDocument())
await waitFor(() => expect(redirectButton()).toBeInTheDocument())
})
......@@ -31,6 +31,7 @@ import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
import Markdown from '../Markdown'
import DialogActions from '@material-ui/core/DialogActions'
import Button from '@material-ui/core/Button'
import LaunchIcon from '@material-ui/icons/Launch'
const HelpDialog = React.memo(({title, description}) => {
const [open, setOpen] = useState(false)
......@@ -195,3 +196,71 @@ export const StringField = React.memo((props) => {
StringField.propTypes = {
onChange: PropTypes.func
}
export const TextFieldWithLinkButton = React.memo(React.forwardRef((props, ref) => {
const {withOtherAdornment, label, value, helpDescription, 'data-testid': TestId, ...otherProps} = props
const classes = useWithHelpStyles()
const validateURL = useCallback((value) => {
try {
return Boolean(new URL(value))
} catch (e) {
return false
}
}, [])
return <TextField
defaultValue={value}
error={value !== undefined && !validateURL(value)}
helperText={value === undefined || validateURL(value) ? '' : 'Invalid URL string!'}
inputRef={ref}
className={classes.root}
InputProps={{endAdornment:
<>
{ helpDescription &&
<div id="help">
<HelpAdornment title={label} description={helpDescription} withOtherAdornment={withOtherAdornment}/>
</div>
}
{
validateURL(value) &&
<IconButton aria-label="open_link" onClick={() => window.open(value, '_blank')}>
<LaunchIcon />
</IconButton>
}
</>
}}
label={label}
data-testid={'URLEditQuantity'}
{...otherProps}
/>
}))
TextFieldWithLinkButton.propTypes = {
withOtherAdornment: PropTypes.bool,
label: PropTypes.string,
value: PropTypes.string,
helpDescription: PropTypes.string,
'data-testid': PropTypes.string
}
export const URLEditQuantity = React.memo((props) => {
const {quantityDef, onChange, ...otherProps} = props
const handleChange = useCallback((value) => {
if (onChange) {
onChange(value === '' ? undefined : value)
}
}, [onChange])
return <TextFieldWithLinkButton
fullWidth variant='filled' size='small'
onChange={event => handleChange(event.target.value)}
{...getFieldProps(quantityDef)}
{...otherProps}
/>
})
URLEditQuantity.propTypes = {
quantityDef: PropTypes.object.isRequired,
value: PropTypes.string,
onChange: PropTypes.func
}
......@@ -54,6 +54,7 @@ from .metainfo import (
SectionReference,
QuantityReference,
File,
URL,
Datetime,
Unit,
JSON,
......
......@@ -851,6 +851,40 @@ class _File(DataType):
return value
class _URL(DataType):
def _validate_web_url(self, url_str: str):
urlRegex = re.compile(
r'^(?:http|ftp)s?://'
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'
r'localhost|'
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
r'(?::\d+)?'
r'(?:/?|[/?]\S+)$', re.IGNORECASE)
return (re.match(urlRegex, url_str) is not None)
def _test(self, url_str: str) -> str:
if url_str is None:
return None
if not isinstance(url_str, str):
raise TypeError('Links need to be given as URL strings')
if not self._validate_web_url(url_str):
raise ValueError('The given URL is not valid')
return url_str
def set_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any:
return self._test(value)
def serialize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any:
if value is None:
return None
return self._test(value)
def deserialize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any:
return self._test(value)
class _Datetime(DataType):
def _parse(self, datetime_str: str) -> datetime:
......@@ -965,10 +999,11 @@ JSON = _JSON()
Capitalized = _Capitalized()
Bytes = _Bytes()
File = _File()
URL = _URL()
predefined_datatypes = {
'Dimension': Dimension, 'Unit': Unit, 'Datetime': Datetime,
'JSON': JSON, 'Capitalized': Capitalized, 'bytes': Bytes, 'File': File}
'JSON': JSON, 'Capitalized': Capitalized, 'bytes': Bytes, 'File': File, 'URL': URL}
# Metainfo data storage and reflection interface
......
......@@ -20,7 +20,7 @@ import pytest
import json
import datetime
import pytz
from nomad.metainfo import MSection, Quantity, Unit, units, JSON, Dimension, Datetime, Capitalized, Bytes
from nomad.metainfo import MSection, Quantity, Unit, units, JSON, Dimension, Datetime, Capitalized, Bytes, URL
@pytest.mark.parametrize('def_type, value', [
......@@ -33,6 +33,7 @@ from nomad.metainfo import MSection, Quantity, Unit, units, JSON, Dimension, Dat
pytest.param(Dimension, '*', id='Dimension-*'),
pytest.param(Dimension, 1, id='Dimension-1'),
pytest.param(Dimension, 'quantity', id='Dimension-quantity'),
pytest.param(URL, 'http://google.com', id='Url-link'),
pytest.param(Datetime, datetime.datetime.now(datetime.timezone.utc), id='Datetime'),
pytest.param(Datetime, datetime.datetime.now(pytz.timezone('America/Los_Angeles')), id='Datetime'),
pytest.param(Datetime, datetime.date.today(), id='Date'),
......@@ -78,7 +79,8 @@ def test_basic_types(def_type, value):
pytest.param(Datetime, '1970-01-01T00:00:00Z', None, id='Datetime-aniso861-time'),
pytest.param(Datetime, '1970-01-01', None, id='Datetime-aniso861-date'),
pytest.param(Datetime, '2022-05-19T05:16:32.237914-07:00', None, id='Datetime-conversion-from-localtime-to-UTC'),
pytest.param(Capitalized, 'hello', 'Hello', id='Capitalize')
pytest.param(Capitalized, 'hello', 'Hello', id='Capitalize'),
pytest.param(URL, 'http://google.com', 'http://google.com', id='URL')
])
def test_value_normalization(def_type, orig_value, normalized_value):
class TestSection(MSection):
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment