From d8e66885963cc51bfeea66afd0067117fe0342c9 Mon Sep 17 00:00:00 2001 From: "eniko.pusztai" Date: Wed, 3 Mar 2021 09:01:17 +0100 Subject: [PATCH] Fix minor issues (#1188) * fix minor issues * fix findings Co-authored-by: Zoltan Takacs --- .../src/fieldcontrols/file-size.tsx | 110 ++++++++++++++ .../src/fieldcontrols/index.ts | 1 + .../src/fieldcontrols/localization.ts | 3 + .../src/fieldcontrols/switcher.tsx | 4 +- .../src/react-control-mapper.ts | 8 +- .../__snapshots__/file-size.test.tsx.snap | 122 ++++++++++++++++ .../sn-controls-react/test/file-size.test.tsx | 137 ++++++++++++++++++ 7 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 packages/sn-controls-react/src/fieldcontrols/file-size.tsx create mode 100644 packages/sn-controls-react/test/__snapshots__/file-size.test.tsx.snap create mode 100644 packages/sn-controls-react/test/file-size.test.tsx diff --git a/packages/sn-controls-react/src/fieldcontrols/file-size.tsx b/packages/sn-controls-react/src/fieldcontrols/file-size.tsx new file mode 100644 index 000000000..c7158649e --- /dev/null +++ b/packages/sn-controls-react/src/fieldcontrols/file-size.tsx @@ -0,0 +1,110 @@ +/** + * @module FieldControls + */ +import { deepMerge, toNumber } from '@sensenet/client-utils' +import { NumberFieldSetting } from '@sensenet/default-content-types' +import FormHelperText from '@material-ui/core/FormHelperText' +import InputAdornment from '@material-ui/core/InputAdornment' +import TextField from '@material-ui/core/TextField' +import Typography from '@material-ui/core/Typography' +import React, { useState } from 'react' +import { changeTemplatedValue } from '../helpers' +import { ReactClientFieldSetting } from './client-field-setting' +import { defaultLocalization } from './localization' + +const units = ['byte', 'KB', 'MB', 'GB', 'TB'] + +/** + * Field control that represents a Number field. Available values will be populated from the FieldSettings. + */ +export const FileSizeField: React.FC> = (props) => { + const localization = deepMerge(defaultLocalization.fileSize, props.localization?.fileSize) + + const initialState = + props.fieldValue != null + ? props.fieldValue + : (props.actionName === 'new' && + props.settings.DefaultValue !== undefined && + Number.parseInt(changeTemplatedValue(props.settings.DefaultValue)!, 10)) || + undefined + const [value, setValue] = useState(initialState) + + const handleChange = (e: React.ChangeEvent) => { + setValue(e.target.value) + props.fieldOnChange?.(props.settings.Name, e.target.value) + } + + /** + * Returns steps value by decimal and step settings + */ + const defineStepValue = () => { + if (props.settings.Step) { + return props.settings.Step + } + if (!props.fieldValue) { + return 1 + } + return Number.isInteger(toNumber(props.fieldValue)!) || props.settings.Type === 'IntegerFieldSetting' ? 1 : 0.1 + } + + const round = (num: number, precision = 1) => { + const multiplier = Math.pow(10, precision) + return Math.round((num + Number.EPSILON) * multiplier) / multiplier + } + + const returnValueWithUnit = (fieldValueNumber: number, index = 0): string => { + const inHigherUnit = round(fieldValueNumber / 1024) + if (inHigherUnit >= 1 && units.length > index + 1) { + return returnValueWithUnit(inHigherUnit, index + 1) + } else { + return `${fieldValueNumber} ${units[index]}` + } + } + + switch (props.actionName) { + case 'edit': + case 'new': + return ( + <> + byte, + }} + inputProps={{ + step: defineStepValue(), + max: props.settings.MaxValue, + min: props.settings.MinValue, + }} + id={props.settings.Name} + fullWidth={true} + onChange={handleChange} + /> + {!props.hideDescription && {props.settings.Description}} + + ) + case 'browse': + default: + return ( +
+ + {props.settings.DisplayName} + + + {props.fieldValue && props.fieldValue !== '0' ? ( + <>{returnValueWithUnit(toNumber(props.fieldValue)!, 0)} + ) : ( + localization.noValue + )} + +
+ ) + } +} diff --git a/packages/sn-controls-react/src/fieldcontrols/index.ts b/packages/sn-controls-react/src/fieldcontrols/index.ts index 6e943134e..fb16dc9df 100644 --- a/packages/sn-controls-react/src/fieldcontrols/index.ts +++ b/packages/sn-controls-react/src/fieldcontrols/index.ts @@ -31,3 +31,4 @@ export * from './allowed-child-types' export * from './client-field-setting' export * from './icon' export * from './localization' +export * from './file-size' diff --git a/packages/sn-controls-react/src/fieldcontrols/localization.ts b/packages/sn-controls-react/src/fieldcontrols/localization.ts index 4a598fc5c..56e58d9e1 100644 --- a/packages/sn-controls-react/src/fieldcontrols/localization.ts +++ b/packages/sn-controls-react/src/fieldcontrols/localization.ts @@ -70,6 +70,9 @@ export const defaultLocalization = { timePicker: { noValue: 'No time selected', }, + fileSize: { + noValue: 'No value set', + }, } export type FieldLocalization = DeepPartial diff --git a/packages/sn-controls-react/src/fieldcontrols/switcher.tsx b/packages/sn-controls-react/src/fieldcontrols/switcher.tsx index 23033b580..39d8aaa06 100644 --- a/packages/sn-controls-react/src/fieldcontrols/switcher.tsx +++ b/packages/sn-controls-react/src/fieldcontrols/switcher.tsx @@ -48,10 +48,10 @@ export const Switch = withStyles((theme: Theme) => ({ opacity: 1, '&$checked': { transform: 'translateX(12px)', - color: theme.palette.type === 'light' ? theme.palette.common.white : theme.palette.common.black, + color: theme.palette.common.white, '& + $track': { opacity: 1, - backgroundColor: theme.palette.type === 'light' ? theme.palette.common.black : theme.palette.common.white, + backgroundColor: theme.palette.primary.main, borderColor: theme.palette.type === 'light' ? theme.palette.common.black : theme.palette.common.white, }, }, diff --git a/packages/sn-controls-react/src/react-control-mapper.ts b/packages/sn-controls-react/src/react-control-mapper.ts index e67d0ae9c..fba566733 100644 --- a/packages/sn-controls-react/src/react-control-mapper.ts +++ b/packages/sn-controls-react/src/react-control-mapper.ts @@ -29,8 +29,12 @@ export const reactControlMapper = (repository: Repository) => { () => null, ) controlMapper - .setupFieldSettingDefault('NumberFieldSetting', () => { - return FieldControls.NumberField + .setupFieldSettingDefault('NumberFieldSetting', (setting) => { + if (setting.ControlHint === 'sn:FileSize') { + return FieldControls.FileSizeField + } else { + return FieldControls.NumberField + } }) .setupFieldSettingDefault('CurrencyFieldSetting', () => { return FieldControls.NumberField diff --git a/packages/sn-controls-react/test/__snapshots__/file-size.test.tsx.snap b/packages/sn-controls-react/test/__snapshots__/file-size.test.tsx.snap new file mode 100644 index 000000000..c56275f61 --- /dev/null +++ b/packages/sn-controls-react/test/__snapshots__/file-size.test.tsx.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FileSize field control in browse view should show the displayname and fieldValue with GB unit when fieldValue is provided and between 1 073 741 824 and 1 099 511 627 776 {pow(1024,4)} 1`] = ` +
+ + Size + + + 2 GB + +
+`; + +exports[`FileSize field control in browse view should show the displayname and fieldValue with KB unit when fieldValue is provided and between 1024 and 1 048 576 {pow(1024,2)} 1`] = ` +
+ + Size + + + 1.5 KB + +
+`; + +exports[`FileSize field control in browse view should show the displayname and fieldValue with MB unit when fieldValue is provided and between 1 048 576 and 1 073 741 824 {pow(1024,3)} 1`] = ` +
+ + Size + + + 1 MB + +
+`; + +exports[`FileSize field control in browse view should show the displayname and fieldValue with TB unit when fieldValue is provided and between 1 099 511 627 776 and 1 125 899 906 842 624 {pow(1024,5)} 1`] = ` +
+ + Size + + + 653.5 TB + +
+`; + +exports[`FileSize field control in browse view should show the displayname and fieldValue with byte unit when fieldValue is provided and less than 1024 1`] = ` +
+ + Size + + + 123 byte + +
+`; + +exports[`FileSize field control in edit/new view should set all the props 1`] = ` + + + byte + , + } + } + disabled={true} + fullWidth={true} + id="Size" + inputProps={ + Object { + "max": 50, + "min": 4, + "step": 1, + } + } + label="Size" + name="Size" + onChange={[Function]} + placeholder="Size" + required={true} + type="number" + value={7} + /> + + File size + + +`; diff --git a/packages/sn-controls-react/test/file-size.test.tsx b/packages/sn-controls-react/test/file-size.test.tsx new file mode 100644 index 000000000..97bbec16e --- /dev/null +++ b/packages/sn-controls-react/test/file-size.test.tsx @@ -0,0 +1,137 @@ +import { toNumber } from '@sensenet/client-utils' +import FormHelperText from '@material-ui/core/FormHelperText' +import TextField from '@material-ui/core/TextField' +import Typography from '@material-ui/core/Typography' +import { shallow } from 'enzyme' +import React from 'react' +import { defaultLocalization, FileSizeField } from '../src/fieldcontrols' + +const round = (num: number, precision = 1) => { + const multiplier = Math.pow(10, precision) + return Math.round((num + Number.EPSILON) * multiplier) / multiplier +} + +describe('FileSize field control', () => { + const defaultSettings = { + Name: 'Size', + FieldClassName: 'SenseNet.ContentRepository.Fields.NumberField', + DisplayName: 'Size', + Description: 'File size', + Type: 'NumberFieldSetting', + } + + describe('in browse view', () => { + it('should show the displayname and fieldValue with byte unit when fieldValue is provided and less than 1024', () => { + const value = '123' + const wrapper = shallow() + expect(wrapper.find(Typography).first().text()).toBe(defaultSettings.DisplayName) + expect(wrapper.find(Typography).last().text()).toBe(`${value} byte`) + expect(wrapper).toMatchSnapshot() + }) + + it('should show the displayname and fieldValue with KB unit when fieldValue is provided and between 1024 and 1 048 576 {pow(1024,2)}', () => { + const value = '1500' + const wrapper = shallow() + expect(wrapper.find(Typography).first().text()).toBe(defaultSettings.DisplayName) + expect(wrapper.find(Typography).last().text()).toBe(`${round(toNumber(value) / 1024)} KB`) + expect(wrapper).toMatchSnapshot() + }) + + it('should show the displayname and fieldValue with MB unit when fieldValue is provided and between 1 048 576 and 1 073 741 824 {pow(1024,3)}', () => { + const value = '1050000' + const wrapper = shallow() + expect(wrapper.find(Typography).first().text()).toBe(defaultSettings.DisplayName) + expect(wrapper.find(Typography).last().text()).toBe(`${round(toNumber(value) / 1024 / 1024)} MB`) + expect(wrapper).toMatchSnapshot() + }) + + it('should show the displayname and fieldValue with GB unit when fieldValue is provided and between 1 073 741 824 and 1 099 511 627 776 {pow(1024,4)}', () => { + const value = '2147483648' + const wrapper = shallow() + expect(wrapper.find(Typography).first().text()).toBe(defaultSettings.DisplayName) + expect(wrapper.find(Typography).last().text()).toBe(`${round(toNumber(value) / 1024 / 1024 / 1024)} GB`) + expect(wrapper).toMatchSnapshot() + }) + + it('should show the displayname and fieldValue with TB unit when fieldValue is provided and between 1 099 511 627 776 and 1 125 899 906 842 624 {pow(1024,5)}', () => { + const value = '718552496548997' + const wrapper = shallow() + expect(wrapper.find(Typography).first().text()).toBe(defaultSettings.DisplayName) + expect(wrapper.find(Typography).last().text()).toBe(`${round(toNumber(value) / 1024 / 1024 / 1024 / 1024)} TB`) + expect(wrapper).toMatchSnapshot() + }) + + it('should show no value message when field value is not provided', () => { + const wrapper = shallow() + expect(wrapper.find(Typography).last().text()).toBe(defaultLocalization.fileSize.noValue) + }) + + it('should show no value message when field value is undefined', () => { + const value = undefined + const wrapper = shallow() + expect(wrapper.find(Typography).last().text()).toBe(defaultLocalization.fileSize.noValue) + }) + + it('should show no value message when field value is null', () => { + const value = null + const wrapper = shallow() + expect(wrapper.find(Typography).last().text()).toBe(defaultLocalization.fileSize.noValue) + }) + + it('should show no value message when field value is 0', () => { + const value = '0' + const wrapper = shallow() + expect(wrapper.find(Typography).last().text()).toBe(defaultLocalization.fileSize.noValue) + }) + }) + describe('in edit/new view', () => { + it('should set all the props', () => { + const wrapper = shallow( + , + ) + expect(wrapper.find(TextField).prop('value')).toBe(7) + expect(wrapper.find(TextField).prop('name')).toBe(defaultSettings.Name) + expect(wrapper.find(TextField).prop('id')).toBe(defaultSettings.Name) + expect(wrapper.find(TextField).prop('label')).toBe(defaultSettings.DisplayName) + expect(wrapper.find(TextField).prop('placeholder')).toBe(defaultSettings.DisplayName) + expect(wrapper.find(TextField).prop('required')).toBeTruthy() + expect(wrapper.find(TextField).prop('disabled')).toBeTruthy() + expect(wrapper.find(FormHelperText).text()).toBe(defaultSettings.Description) + expect(wrapper).toMatchSnapshot() + }) + + it('should set default value', () => { + const wrapper = shallow( + , + ) + + expect(wrapper.find(TextField).prop('value')).toEqual(7) + }) + + it('should call on change when input changes', () => { + const fieldOnChange = jest.fn() + const wrapper = shallow( + , + ) + wrapper.find(TextField).simulate('change', { target: { value: 2 } }) + expect(fieldOnChange).toBeCalled() + }) + }) +})