Skip to content

Commit 3d7e26e

Browse files
authored
Merge pull request #168 from performant-software/feature/udf5_configure_fields
UDF #5 - Configure fields
2 parents 1c01a1d + 93b02c0 commit 3d7e26e

22 files changed

+658
-9
lines changed

packages/semantic-ui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@performant-software/semantic-components",
3-
"version": "0.6.0",
3+
"version": "0.6.1",
44
"description": "A package of shared components based on the Semantic UI Framework.",
55
"license": "MIT",
66
"main": "./build/index.js",
@@ -12,7 +12,7 @@
1212
"build": "webpack --mode production && flow-copy-source -v src types"
1313
},
1414
"dependencies": {
15-
"@performant-software/shared-components": "^0.6.0",
15+
"@performant-software/shared-components": "^0.6.1",
1616
"@react-google-maps/api": "^2.8.1",
1717
"axios": "^0.26.1",
1818
"i18next": "^19.4.4",
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// @flow
2+
3+
import { UserDefinedFieldsService } from '@performant-software/shared-components';
4+
import React, {
5+
useCallback,
6+
useEffect,
7+
useState,
8+
type ComponentType
9+
} from 'react';
10+
import { Form, Modal } from 'semantic-ui-react';
11+
import _ from 'underscore';
12+
import i18n from '../i18n/i18n';
13+
import UserDefinedFieldOptions from './UserDefinedFieldOptions';
14+
15+
const DataTypes = {
16+
select: 'Select'
17+
};
18+
19+
const UserDefinedFieldModal: ComponentType<any> = (props) => {
20+
const [dataTypeOptions, setDataTypeOptions] = useState([]);
21+
const [tableOptions, setTableOptions] = useState([]);
22+
23+
/**
24+
* Transforms the passed array of items into a Semantic UI option object.
25+
*
26+
* @type {function(*): *}
27+
*/
28+
const transformOptions = useCallback((items) => (
29+
_.map(items, (item) => ({ key: item, value: item, text: item }))
30+
), []);
31+
32+
/**
33+
* Fetch the available tables and data types.
34+
*/
35+
useEffect(() => {
36+
UserDefinedFieldsService
37+
.fetchTables()
38+
.then(({ data }) => setTableOptions(transformOptions(data.tables)));
39+
40+
UserDefinedFieldsService
41+
.fetchDataTypes()
42+
.then(({ data }) => setDataTypeOptions(transformOptions(data.data_types)));
43+
}, []);
44+
45+
return (
46+
<Modal
47+
as={Form}
48+
centered={false}
49+
open
50+
>
51+
<Modal.Header
52+
content={props.item.id
53+
? i18n.t('UserDefinedFieldModal.title.edit')
54+
: i18n.t('UserDefinedFieldModal.title.add')}
55+
/>
56+
<Modal.Content>
57+
<Form.Dropdown
58+
clearable
59+
error={props.isError('table_name')}
60+
label={i18n.t('UserDefinedFieldModal.labels.table')}
61+
onChange={props.onTextInputChange.bind(this, 'table_name')}
62+
options={tableOptions}
63+
required={props.isRequired('table_name')}
64+
selection
65+
selectOnBlur={false}
66+
value={props.item.table_name || ''}
67+
/>
68+
<Form.Input
69+
error={props.isError('column_name')}
70+
label={i18n.t('UserDefinedFieldModal.labels.name')}
71+
onChange={props.onTextInputChange.bind(this, 'column_name')}
72+
required={props.isRequired('column_name')}
73+
value={props.item.column_name || ''}
74+
/>
75+
<Form.Dropdown
76+
clearable
77+
error={props.isError('data_type')}
78+
label={i18n.t('UserDefinedFieldModal.labels.dataType')}
79+
onChange={props.onTextInputChange.bind(this, 'data_type')}
80+
options={dataTypeOptions}
81+
required={props.isRequired('data_type')}
82+
selection
83+
selectOnBlur={false}
84+
value={props.item.data_type || ''}
85+
/>
86+
<Form.Group>
87+
<Form.Checkbox
88+
error={props.isError('required')}
89+
checked={props.item.required}
90+
label={i18n.t('UserDefinedFieldModal.labels.required')}
91+
onChange={props.onCheckboxInputChange.bind(this, 'required')}
92+
/>
93+
{ props.item.data_type === DataTypes.select && (
94+
<Form.Checkbox
95+
error={props.isError('allow_multiple')}
96+
checked={props.item.allow_multiple}
97+
label={i18n.t('UserDefinedFieldModal.labels.allowMultiple')}
98+
onChange={props.onCheckboxInputChange.bind(this, 'allow_multiple')}
99+
/>
100+
)}
101+
</Form.Group>
102+
{ props.item.data_type === DataTypes.select && (
103+
<UserDefinedFieldOptions
104+
options={props.item.options}
105+
onChange={(options) => props.onSetState({ options })}
106+
/>
107+
)}
108+
</Modal.Content>
109+
{ props.children }
110+
</Modal>
111+
);
112+
};
113+
114+
export default UserDefinedFieldModal;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.user-defined-field-options .label .input {
2+
margin-right: 0.5em;
3+
width: unset;
4+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// @flow
2+
3+
import React, {
4+
useCallback,
5+
useEffect,
6+
useState,
7+
type ComponentType
8+
} from 'react';
9+
import { Button, Input, Label } from 'semantic-ui-react';
10+
import _ from 'underscore';
11+
import './UserDefinedFieldOptions.css';
12+
13+
type Props = {
14+
options: Array<string>,
15+
onChange: (options: Array<string>) => void
16+
};
17+
18+
const UserDefinedFieldOptions: ComponentType<any> = (props: Props) => {
19+
const [options, setOptions] = useState(_.map(props.options, (option) => ({ value: option })));
20+
21+
/**
22+
* Adds a new option to the list.
23+
*
24+
* @type {(function(): void)|*}
25+
*/
26+
const onAddOption = useCallback(() => {
27+
setOptions((prevOptions) => [...prevOptions, { new: true }]);
28+
}, []);
29+
30+
/**
31+
* Deletes the option at the passed index from the list.
32+
*
33+
* @type {(function(*): void)|*}
34+
*/
35+
const onDeleteOption = useCallback((findIndex) => {
36+
setOptions((prevOptions) => _.filter(prevOptions, (option, index) => index !== findIndex));
37+
}, []);
38+
39+
/**
40+
* Removes the "new" indicator from the option at the passed index.
41+
*
42+
* @type {(function(*): void)|*}
43+
*/
44+
const onSaveOption = useCallback((findIndex) => {
45+
setOptions((prevOptions) => _.map(
46+
prevOptions,
47+
(option, index) => (findIndex !== index ? option : ({ ...option, new: false }))
48+
));
49+
}, [options]);
50+
51+
/**
52+
* Updates the value of the option at the passed index.
53+
*
54+
* @type {(function(*, *, {value: *}): void)|*}
55+
*/
56+
const onUpdateOption = useCallback((findIndex, e, { value }) => {
57+
setOptions((prevOptions) => _.map(
58+
prevOptions,
59+
(option, index) => (index !== findIndex ? option : ({ ...option, value }))
60+
));
61+
}, []);
62+
63+
/**
64+
* Calls the onChange prop when the list of options changes.
65+
*/
66+
useEffect(() => {
67+
const savedOptions = _.filter(options, (option) => !option.new);
68+
props.onChange(_.pluck(savedOptions, 'value'));
69+
}, [options]);
70+
71+
return (
72+
<div
73+
className='user-defined-field-options'
74+
>
75+
<Button
76+
basic
77+
icon='plus'
78+
onClick={onAddOption}
79+
type='button'
80+
/>
81+
{ _.map(options, (option, index) => (
82+
<>
83+
{ option.new && (
84+
<Label>
85+
<Input
86+
autoFocus
87+
onChange={onUpdateOption.bind(this, index)}
88+
value={option.value}
89+
/>
90+
<Button
91+
basic
92+
color='green'
93+
icon='checkmark'
94+
onClick={onSaveOption.bind(this, index)}
95+
type='button'
96+
size='tiny'
97+
/>
98+
</Label>
99+
)}
100+
{ !option.new && (
101+
<Label
102+
content={option.value}
103+
onRemove={onDeleteOption.bind(this, index)}
104+
/>
105+
)}
106+
</>
107+
))}
108+
</div>
109+
);
110+
};
111+
112+
export default UserDefinedFieldOptions;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// @flow
2+
3+
import React from 'react';
4+
import i18n from '../i18n/i18n';
5+
import BooleanIcon from './BooleanIcon';
6+
import EmbeddedList from './EmbeddedList';
7+
import UserDefinedFieldModal from './UserDefinedFieldModal';
8+
9+
type Props = {
10+
items: Array<any>,
11+
onDelete: (item: any) => Promise<any>,
12+
onSave: (item: any) => Promise<any>
13+
};
14+
15+
const UserDefinedFieldsEmbeddedList = (props: Props) => (
16+
<EmbeddedList
17+
actions={[{
18+
name: 'edit'
19+
}, {
20+
name: 'delete'
21+
}]}
22+
columns={[{
23+
name: 'table_name',
24+
label: i18n.t('UserDefinedFieldsEmbeddedList.columns.table')
25+
}, {
26+
name: 'column_name',
27+
label: i18n.t('UserDefinedFieldsEmbeddedList.columns.name')
28+
}, {
29+
name: 'data_type',
30+
label: i18n.t('UserDefinedFieldsEmbeddedList.columns.dataType')
31+
}, {
32+
name: 'required',
33+
label: i18n.t('UserDefinedFieldsEmbeddedList.columns.required'),
34+
render: (udf) => <BooleanIcon value={udf.required} />
35+
}]}
36+
items={props.items}
37+
modal={{
38+
component: UserDefinedFieldModal
39+
}}
40+
onDelete={props.onDelete}
41+
onSave={props.onSave}
42+
/>
43+
);
44+
45+
export default UserDefinedFieldsEmbeddedList;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @flow
2+
3+
import { UserDefinedFieldsService } from '@performant-software/shared-components';
4+
import React, { type ComponentType } from 'react';
5+
import i18n from '../i18n/i18n';
6+
import BooleanIcon from './BooleanIcon';
7+
import ListTable from './ListTable';
8+
import UserDefinedFieldModal from './UserDefinedFieldModal';
9+
10+
const UserDefinedFieldsList: ComponentType<any> = () => (
11+
<ListTable
12+
actions={[{
13+
name: 'edit'
14+
}, {
15+
name: 'delete'
16+
}]}
17+
columns={[{
18+
name: 'table_name',
19+
label: i18n.t('UserDefinedFieldsList.columns.table')
20+
}, {
21+
name: 'column_name',
22+
label: i18n.t('UserDefinedFieldsList.columns.name')
23+
}, {
24+
name: 'data_type',
25+
label: i18n.t('UserDefinedFieldsList.columns.dataType')
26+
}, {
27+
name: 'required',
28+
label: i18n.t('UserDefinedFieldsList.columns.required'),
29+
render: (udf) => <BooleanIcon value={udf.required} />
30+
}]}
31+
collectionName='user_defined_fields'
32+
modal={{
33+
component: UserDefinedFieldModal
34+
}}
35+
onLoad={(params) => UserDefinedFieldsService.fetchAll(params)}
36+
onSave={(udf) => UserDefinedFieldsService.save(udf)}
37+
onDelete={(udf) => UserDefinedFieldsService.delete(udf)}
38+
/>
39+
);
40+
41+
export default UserDefinedFieldsList;

packages/semantic-ui/src/i18n/en.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,35 @@
255255
"Selectize": {
256256
"noRecords": "No matching records."
257257
},
258+
"UserDefinedFieldModal": {
259+
"labels": {
260+
"allowMultiple": "Allow multiple",
261+
"dataType": "Data type",
262+
"name": "Name",
263+
"required": "Required",
264+
"table": "Table"
265+
},
266+
"title": {
267+
"add": "Add User Defined Field",
268+
"edit": "Edit User Defined Field"
269+
}
270+
},
271+
"UserDefinedFieldsEmbeddedList": {
272+
"columns": {
273+
"dataType": "Data type",
274+
"name": "Name",
275+
"required": "Required",
276+
"table": "Table"
277+
}
278+
},
279+
"UserDefinedFieldsList": {
280+
"columns": {
281+
"dataType": "Data type",
282+
"name": "Name",
283+
"required": "Required",
284+
"table": "Table"
285+
}
286+
},
258287
"VideoFrameSelector": {
259288
"buttons": {
260289
"select": "Select frame"

packages/semantic-ui/src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export { default as TabsMenu } from './components/TabsMenu';
8080
export { default as TagsList } from './components/TagsList';
8181
export { default as Thumbnail } from './components/Thumbnail';
8282
export { default as Toaster } from './components/Toaster';
83+
export { default as UserDefinedFieldModal } from './components/UserDefinedFieldModal';
84+
export { default as UserDefinedFieldOptions } from './components/UserDefinedFieldOptions';
85+
export { default as UserDefinedFieldsEmbeddedList } from './components/UserDefinedFieldsEmbeddedList';
86+
export { default as UserDefinedFieldsList } from './components/UserDefinedFieldsList';
8387
export { default as VideoFrameSelector } from './components/VideoFrameSelector';
8488
export { default as VideoPlayer } from './components/VideoPlayer';
8589
export { default as VideoPlayerButton } from './components/VideoPlayerButton';

packages/shared/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@performant-software/shared-components",
3-
"version": "0.6.0",
3+
"version": "0.6.1",
44
"description": "A package of shared, framework agnostic, components.",
55
"license": "MIT",
66
"main": "./build/index.js",

0 commit comments

Comments
 (0)