License updated to GPLv3

🧲 New features
Custom user role permissions
Employee edit form updated
Employee daily task list
Attendance and employee distribution charts on dashboard
Improvements to company structure and company assets module
Improved tables for displaying data in several modules
Faster data loading (specially for employee module)
Initials based profile pictures
Re-designed login page
Re-designed user profile page
Improvements to filtering
New REST endpoints for employee qualifications

🐛 Bug fixes
Fixed, issue with managers being able to create performance reviews for employees who are not their direct reports
Fixed, issues related to using full profile image instead of using smaller version of profile image
Changing third gender to other
Improvements and fixes for internal frontend data caching
This commit is contained in:
Thilina Pituwala
2020-10-31 19:02:37 +01:00
parent 86b8345505
commit b1df0037db
29343 changed files with 867614 additions and 2191082 deletions

View File

@@ -0,0 +1,244 @@
import React from "react";
import {Button, Form, Select, Space, Card, Modal, Table} from "antd";
// import IceDataGroupModal from "./IceDataGroupModal";
import IceFormModal from "./IceFormModal";
import ReactDOM from "react-dom";
const { Option } = Select;
class IceDataGroup extends React.Component {
state = {};
constructor(props) {
super(props);
this.onChange = props.onChange;
this.formReference = React.createRef();
}
render() {
const { field, adapter } = this.props;
let { value } = this.props;
value = this.parseValue(value);
value = value.map(item => ({ ...item, key:item.id } ));
const columns = JSON.parse(JSON.stringify(field[1].columns));
if (!this.props.readOnly) {
columns.push({
title: 'Action',
key: 'action',
render: (text, record) => (
this.getDefaultButtons(record.id)
),
});
}
return (
<>
{!this.props.readOnly &&
<Space direction="horizontal">
<Button type="link" htmlType="button" onClick={() => {
this.createForm(field, adapter, {})
}}>
Add
</Button>
<Button type="link" htmlType="button" danger onClick={() => {
this.resetDataGroup()
}}>
Reset
</Button>
</Space>
}
<Table columns={columns} dataSource={value} />
</>
);
}
createForm(field, adapter, object) {
this.formContainer = React.createRef();
const formFields = field[1].form;
formFields.unshift(['id', { label: 'ID', type: 'hidden' }]);
ReactDOM.render(
<IceFormModal
ref={this.formContainer}
fields={formFields}
title={this.props.title}
adapter={adapter}
formReference={this.formReference}
saveCallback={this.save.bind(this)}
cancelCallback={this.unmountForm.bind(this)}
/>,
document.getElementById('dataGroup'),
);
this.formContainer.current.show(object);
}
unmountForm() {
ReactDOM.unmountComponentAtNode(document.getElementById('dataGroup'));
}
show(data) {
if (!data) {
this.setState({ visible: true });
this.updateFields(data);
} else {
this.setState({ visible: true });
if (this.formReference.current) {
this.updateFields(data);
} else {
this.waitForIt(
() => this.formReference.current != null,
() => { this.updateFields(data); },
100,
);
}
}
}
parseValue(value) {
try {
value = JSON.parse(value);
} catch (e) {
value = [];
}
if (value == null) {
value = [];
}
return value;
}
save(params, errorCallback, closeCallback) {
const {field, value } = this.props;
if (field[1]['custom-validate-function'] != null) {
let tempParams = field[1]['custom-validate-function'].apply(this, [params]);
if (tempParams.valid) {
params = tempParams.params;
} else {
errorCallback(tempParams.message);
return false;
}
}
const data = this.parseValue(value);
let newData = [];
if (!params.id) {
params.id = `${field[0]}_${this.dataGroupGetNextAutoIncrementId(data)}`;
data.push(params);
newData = data;
} else {
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (item.id !== params.id) {
newData.push(item);
} else {
newData.push(params);
}
}
}
if (field[1]['sort-function'] != null) {
newData.sort(field[1]['sort-function']);
}
const val = JSON.stringify(newData);
this.onChange(val);
this.unmountForm();
}
createCard(item) {
const { field } = this.props;
if (field[1]['pre-format-function'] != null) {
item = field[1]['pre-format-function'].apply(this, [item]);
}
const template = field[1].html;
let t = template.replace('#_delete_#', '');
t = t.replace('#_edit_#', '');
t = t.replace(/#_id_#/g, item.id);
for (const key in item) {
let itemVal = item[key];
if (itemVal !== undefined && itemVal != null && typeof itemVal === 'string') {
itemVal = itemVal.replace(/(?:\r\n|\r|\n)/g, '<br />');
}
t = t.replace(`#_${key}_#`, itemVal);
}
if (field[1].render !== undefined && field[1].render != null) {
t = t.replace('#_renderFunction_#', field[1].render(item));
}
return (
<Card key={item.id} title="" extra={this.getDefaultButtons(item.id)}>
<div dangerouslySetInnerHTML={{ __html: t }}></div>
</Card>
);
}
getDefaultButtons(id) {
return (
<Space>
<a href="#" onClick={() => {this.editDataGroupItem(id)}}><li className="fa fa-edit"/></a>
<a href="#" onClick={() => {this.deleteDataGroupItem(id)}}><li className="fa fa-times"/></a>
</Space>
);
}
deleteDataGroupItem(id) {
const {value} = this.props;
const data = this.parseValue(value);
const newVal = [];
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (item.id !== id) {
newVal.push(item);
}
}
const val = JSON.stringify(newVal);
this.onChange(val);
}
editDataGroupItem(id) {
const { field, adapter, value } = this.props;
const data = this.parseValue(value);
let editVal = {};
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (item.id === id) {
editVal = item;
}
}
this.createForm(field, adapter, editVal);
}
resetDataGroup() {
this.onChange('[]');
}
dataGroupGetNextAutoIncrementId(data) {
let autoId = 1; let id;
for (let i = 0; i < data.length; i++) {
const item = data[i];
if (item.id === undefined || item.id == null) {
item.id = 1;
}
id = item.id.substring(item.id.lastIndexOf('_') + 1, item.id.length);
if (id >= autoId) {
autoId = parseInt(id, 10) + 1;
}
}
return autoId;
}
}
export default IceDataGroup;

454
web/components/IceForm.js Normal file
View File

@@ -0,0 +1,454 @@
import React from 'react';
import {
Alert, Col, DatePicker, TimePicker, Form, Input, Row,
} from 'antd';
import moment from 'moment';
import IceUpload from './IceUpload';
import IceDataGroup from './IceDataGroup';
import IceSelect from './IceSelect';
import IceLabel from './IceLabel';
const ValidationRules = {
float(str) {
const floatstr = /^[-+]?[0-9]+(\.[0-9]+)?$/;
if (str != null && str.match(floatstr)) {
return true;
}
return false;
},
number(str) {
const numstr = /^[0-9]+$/;
if (str != null && str.match(numstr)) {
return true;
}
return false;
},
numberOrEmpty(str) {
if (str === '') {
return true;
}
const numstr = /^[0-9]+$/;
if (str != null && str.match(numstr)) {
return true;
}
return false;
},
email(str) {
const emailPattern = /^\s*[\w\-+_]+(\.[\w\-+_]+)*@[\w\-+_]+\.[\w\-+_]+(\.[\w\-+_]+)*\s*$/;
return str != null && emailPattern.test(str);
},
emailOrEmpty(str) {
if (str === '') {
return true;
}
const emailPattern = /^\s*[\w\-+_]+(\.[\w\-+_]+)*@[\w\-+_]+\.[\w\-+_]+(\.[\w\-+_]+)*\s*$/;
return str != null && emailPattern.test(str);
},
username(str) {
const username = /^[a-zA-Z0-9.-]+$/;
return str != null && username.test(str);
},
};
class IceForm extends React.Component {
constructor(props) {
super(props);
this.validationRules = {};
this.state = {
validations: {},
errorMsg: false,
};
this.formReference = React.createRef();
}
showError(errorMsg) {
this.setState({ errorMsg });
}
hideError() {
this.setState({ errorMsg: false });
}
isReady() {
return this.formReference.current != null;
}
validateFields() {
return this.formReference.current.validateFields();
}
render() {
const { fields, twoColumnLayout } = this.props;
const formInputs1 = [];
const formInputs2 = [];
const columns = !twoColumnLayout ? 1 : 2;
for (let i = 0; i < fields.length; i++) {
const formInput = this.createFromField(fields[i], this.props.viewOnly);
if (formInput != null) {
if (columns === 1) {
formInputs1.push(formInput);
} else if (i % 2 === 0) {
formInputs1.push(formInput);
} else {
formInputs2.push(formInput);
}
}
}
const onFormLayoutChange = () => {};
return (
<Form
ref={this.formReference}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
layout={this.props.layout || 'horizontal'}
initialValues={{ size: 'middle' }}
onValuesChange={onFormLayoutChange}
size="middle"
>
{this.state.errorMsg
&& (
<>
<Alert message={this.state.errorMsg} type="error" showIcon />
<br />
</>
)}
{columns === 1 && formInputs1}
{columns === 2 && (
<Row gutter={16}>
<Col className="gutter-row" span={12}>
{formInputs1}
</Col>
<Col className="gutter-row" span={12}>
{formInputs2}
</Col>
</Row>
)}
</Form>
);
}
isValid() {
return Object.keys(this.validationRules).reduce((acc, fieldName) => acc && (this.state[fieldName] === 'success' || this.state[fieldName] == null), true);
}
validateOnChange(event) {
const validationRule = this.validationRules[event.target.id];
const { validations } = this.state;
if (validationRule) {
if (validationRule.rule(event.target.value)) {
this.state[event.target.id] = 'success';
this.state[`${event.target.id}_message`] = null;
} else {
this.state[event.target.id] = 'error';
this.state[`${event.target.id}_message`] = validationRule.message;
}
}
this.setState({ validations });
}
createFromField(field, viewOnly = false) {
let userId = 0;
const rules = [];
const requiredRule = { required: true };
const [name, data] = field;
const { adapter, layout } = this.props;
let validationRule = null;
data.label = adapter.gt(data.label);
const labelSpan = layout === 'vertical' ? { span: 24 } : { span: 6 };
const tempSelectBoxes = ['select', 'select2', 'select2multi'];
if (tempSelectBoxes.indexOf(data.type) >= 0 && data['allow-null'] === true) {
requiredRule.required = false;
} else if (data.validation === 'none'
|| data.validation === 'emailOrEmpty'
|| data.validation === 'numberOrEmpty'
) {
requiredRule.required = false;
} else {
requiredRule.required = true;
requiredRule.message = this.generateFieldMessage(data.label);
}
rules.push(requiredRule);
if (data.type === 'hidden') {
requiredRule.required = false;
return (
<Form.Item
labelCol={labelSpan}
style={{ display: 'none' }}
label={data.label}
key={name}
name={name}
rules={rules}
>
<Input />
</Form.Item>
);
} if (data.type === 'text') {
if (data.validation) {
data.validation = data.validation.replace('OrEmpty', '');
validationRule = this.getValidationRule(data);
if (validationRule) {
this.validationRules[name] = {
rule: validationRule,
message: `Invalid value for ${data.label}`,
};
}
}
if (validationRule != null) {
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
validateStatus={this.state[name]}
help={this.state[`${name}_message`]}
>
{viewOnly
? <IceLabel />
: <Input onChange={this.validateOnChange.bind(this)} />}
</Form.Item>
);
}
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
>
{viewOnly
? <IceLabel />
: <Input />}
</Form.Item>
);
} if (data.type === 'textarea') {
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
>
{viewOnly
? <IceLabel />
: <Input.TextArea />}
</Form.Item>
);
} if (data.type === 'date') {
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
>
<DatePicker disabled={viewOnly} />
</Form.Item>
);
} if (data.type === 'datetime') {
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
>
<DatePicker format="YYYY-MM-DD HH:mm:ss" disabled={viewOnly} />
</Form.Item>
);
} if (data.type === 'time') {
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
>
<TimePicker
format="HH:mm"
disabled={viewOnly}
/>
</Form.Item>
);
} if (data.type === 'fileupload') {
const currentEmployee = adapter.getCurrentProfile();
if (currentEmployee != null) {
userId = currentEmployee.id;
} else {
userId = adapter.getUser().id * -1;
}
if (data.filetypes == null) {
data.filetypes = '.doc,.docx,.xml,'
+ 'application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,'
+ 'image/*,'
+ '.pdf';
}
return (
<Form.Item
labelCol={labelSpan}
name={name}
key={name}
label={data.label}
>
<IceUpload
user={userId}
fileGroup={adapter.tab}
fileName={name}
adapter={adapter}
accept={data.filetypes}
readOnly={viewOnly}
/>
</Form.Item>
);
} if (data.type === 'datagroup') {
return (
<Form.Item
labelCol={labelSpan}
name={name}
key={name}
label={data.label}
>
<IceDataGroup
adapter={adapter}
field={field}
title={data.label}
readOnly={viewOnly}
/>
</Form.Item>
);
} if (data.type === 'select2' || data.type === 'select' || data.type === 'select2multi') {
return (
<Form.Item
labelCol={labelSpan}
label={data.label}
key={name}
name={name}
rules={rules}
>
<IceSelect
adapter={adapter}
field={field}
readOnly={viewOnly}
/>
</Form.Item>
);
}
return null;
}
generateFieldMessage(label) {
return `${label}: ${this.props.adapter.gt('is required')}`;
}
getValidationRule(data) {
if (ValidationRules[data.validation] == null) {
return null;
}
return ValidationRules[data.validation];
}
dataToFormFields(data, fields) {
for (let i = 0; i < fields.length; i++) {
const [key, formInputData] = fields[i];
if (formInputData.type === 'date') {
data[key] = data[key] ? moment(data[key], 'YYYY-MM-DD') : null;
} else if (formInputData.type === 'datetime') {
data[key] = data[key] ? moment(data[key], 'YYYY-MM-DD HH:mm:ss') : null;
} else if (formInputData.type === 'time') {
data[key] = data[key] ? moment(data[key], 'HH:mm') : null;
}
}
return data;
}
formFieldsToData(params, fields) {
for (let i = 0; i < fields.length; i++) {
const [key, formInputData] = fields[i];
if (formInputData.type === 'date') {
params[key] = params[key] ? params[key].format('YYYY-MM-DD') : 'NULL';
} else if (formInputData.type === 'datetime') {
params[key] = params[key] ? params[key].format('YYYY-MM-DD HH:mm:ss') : 'NULL';
} else if (formInputData.type === 'time') {
params[key] = params[key] ? params[key].format('HH:mm') : 'NULL';
} else if ((formInputData.type === 'select' || formInputData.type === 'select2') && params[key] == null) {
params[key] = 'NULL';
}
}
return params;
}
updateFields(data) {
const { fields } = this.props;
data = this.dataToFormFields(data, fields);
this.formReference.current.resetFields();
if (data == null) {
return;
}
try {
this.formReference.current.setFieldsValue(data);
} catch (e) {
console.log(e);
}
}
resetFields() {
this.formReference.current.resetFields();
}
setFieldsValue(data) {
this.formReference.current.setFieldsValue(data);
}
save(params, success) {
const { adapter, fields } = this.props;
let values = params;
values = adapter.forceInjectValuesBeforeSave(values);
const msg = adapter.doCustomValidation(values);
if (msg !== null) {
this.showError(msg);
return;
}
if (adapter.csrfRequired) {
values.csrf = $(`#${adapter.getTableName()}Form`).data('csrf');
}
const id = (adapter.currentElement != null) ? adapter.currentElement.id : null;
if (id != null && id !== '') {
values.id = id;
}
values = this.formFieldsToData(values, fields);
adapter.add(values, [], () => adapter.get([]), () => {
this.formReference.current.resetFields();
this.showError(false);
success();
});
}
}
export default IceForm;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Button, Drawer } from 'antd';
import IceFormModal from './IceFormModal';
class IceFormDrawer extends IceFormModal {
render() {
const { fields, adapter } = this.props;
return (
<Drawer
title={this.props.adapter.gt(adapter.objectTypeName)}
width={720}
onClose={() => this.hide()}
visible={this.state.visible}
bodyStyle={{ paddingBottom: 80 }}
zIndex={1200}
maskClosable={false}
footer={(
<div
style={{
textAlign: 'right',
}}
>
<Button
onClick={() => this.hide()}
style={{ marginRight: 8 }}
>
Cancel
</Button>
<Button
onClick={() => {
const form = this.formReference.current;
form
.validateFields()
.then((values) => {
this.save(values);
})
.catch((info) => {
// this.showError(`Validate Failed: ${info.errorFields[0].errors[0]}`);
});
}}
type="primary"
>
Submit
</Button>
</div>
)}
>
{this.createForm(fields, adapter)}
</Drawer>
);
}
}
export default IceFormDrawer;

View File

@@ -0,0 +1,150 @@
import React from 'react';
import {
Button, Col, Modal, Row, Space,
} from 'antd';
import IceForm from './IceForm';
class IceFormModal extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
viewOnly: false,
loading: false,
};
this.iceFormReference = React.createRef();
this.width = 800;
}
setViewOnly(value) {
this.setState({ viewOnly: value });
}
show(data) {
if (!data) {
this.setState({ visible: true });
if (this.iceFormReference.current) {
this.iceFormReference.current.resetFields();
}
} else {
this.setState({ visible: true });
if (this.iceFormReference.current && this.iceFormReference.current.isReady()) {
this.iceFormReference.current.updateFields(data);
} else {
this.waitForIt(
() => this.iceFormReference.current && this.iceFormReference.current.isReady(),
() => { this.iceFormReference.current.updateFields(data); },
1000,
);
}
}
}
waitForIt(condition, callback, time) {
setTimeout(() => {
if (condition()) {
callback();
} else {
this.waitForIt(condition, callback, time);
}
}, time);
}
hide() {
this.setState({ visible: false });
}
save(params) {
this.iceFormReference.current.save(params, () => { this.closeModal(); });
}
closeModal() {
this.hide();
this.iceFormReference.current.showError(false);
}
render() {
const {
fields, adapter, saveCallback, cancelCallback,
} = this.props;
const additionalProps = {};
additionalProps.footer = (
<Row gutter={16}>
<Col className="gutter-row" span={12} style={{}} />
<Col className="gutter-row" span={12} style={{ textAlign: 'right' }}>
<Space>
<Button onClick={() => {
if (cancelCallback) {
cancelCallback();
} else {
this.closeModal();
}
}}
>
{this.props.adapter.gt('Cancel')}
</Button>
<Button
loading={this.state.loading}
type="primary"
onClick={() => {
this.setState({ loading: true });
const iceFrom = this.iceFormReference.current;
iceFrom
.validateFields()
.then((values) => {
if (!iceFrom.isValid()) {
this.setState({ loading: false });
return;
}
if (saveCallback) {
saveCallback(values, iceFrom.showError.bind(this), this.closeModal.bind(this));
} else {
this.save(values);
}
this.setState({ loading: false });
})
.catch((info) => {
this.setState({ loading: false });
});
}}
>
{this.state.viewOnly ? this.props.adapter.gt('Done') : this.props.adapter.gt('Save')}
</Button>
</Space>
</Col>
</Row>
);
if (this.state.viewOnly) {
additionalProps.footer = null;
}
return (
<Modal
visible={this.state.visible}
title={this.props.adapter.gt(this.props.title || adapter.objectTypeName)}
maskClosable={false}
width={this.width}
onCancel={() => {
if (cancelCallback) {
cancelCallback();
} else {
this.closeModal();
}
}}
{...additionalProps}
>
<IceForm
ref={this.iceFormReference}
adapter={adapter}
fields={fields}
viewOnly={this.state.viewOnly}
/>
</Modal>
);
}
}
export default IceFormModal;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Space } from 'antd';
class IceLabel extends React.Component {
constructor(props) {
super(props);
}
render() {
const { value } = this.props;
return (
<Space>
<div contentEditable='true' dangerouslySetInnerHTML={{ __html: this.nl2br(value || '') }}></div>
</Space>
);
}
nl2br(str) {
return (`${str}`).replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '<br />');
}
}
export default IceLabel;

113
web/components/IceSelect.js Normal file
View File

@@ -0,0 +1,113 @@
import React from 'react';
import { Form, Select } from 'antd';
const { Option } = Select;
class IceSelect extends React.Component {
constructor(props) {
super(props);
this.onChange = props.onChange;
}
render() {
let options;
const { field, adapter } = this.props;
let { value } = this.props;
const data = field[1];
if (data['remote-source'] != null) {
let key = `${data['remote-source'][0]}_${data['remote-source'][1]}_${data['remote-source'][2]}`;
if (data['remote-source'].length === 4) {
key = `${key}_${data['remote-source'][3]}`;
}
options = adapter.fieldMasterData[key];
} else {
options = data.source;
}
const optionData = this.getFormSelectOptionsRemote(options, field, adapter);
// value should be an array if multi-select
if (data.type === 'select2multi') {
try {
value = JSON.parse(value);
if (value == null) {
value = [];
}
value = value.map((item) => `${item}`);
} catch (e) {
value = [];
}
}
return (
<Select
mode={data.type === 'select2multi' ? 'multiple' : undefined}
showSearch
placeholder={`Select ${data.label}`}
optionFilterProp="children"
filterOption={
(input, option) => input != null
&& option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
value={value}
options={optionData}
allowClear
onChange={this.handleChange.bind(this)}
disabled={this.props.readOnly}
/>
);
}
handleChange(value) {
const { field } = this.props;
const data = field[1];
if (data.type === 'select2multi') {
this.onChange(JSON.stringify(value));
} else {
this.onChange(value);
}
}
makeOption(option) {
return <Option key={`${option[0]}`} value={`${option[0]}`}>{option[1]}</Option>;
}
getFormSelectOptionsRemote(options, field, adapter) {
const optionData = [];
if (Array.isArray(options)) {
for (let i = 0; i < options.length; i++) {
optionData.push({
label: options[i][1],
value: options[i][0],
});
}
} else {
for (const key in options) {
optionData.push({
label: options[key],
value: key,
});
}
}
// if (field[1].sort === 'true') {
// tuples.sort((a, b) => {
// a = a[1];
// b = b[1];
//
// // eslint-disable-next-line no-nested-ternary
// return a < b ? -1 : (a > b ? 1 : 0);
// });
// }
// for (let i = 0; i < tuples.length; i++) {
// const prop = tuples[i][0];
// const value = tuples[i][1];
// optionData.push([prop, adapter.gt(value)]);
// }
return optionData;
}
}
export default IceSelect;

View File

@@ -0,0 +1,242 @@
import React from 'react';
import {
Button, Divider, Steps, Row, Col, Space,
} from 'antd';
import IceForm from './IceForm';
const { Step } = Steps;
class IceStepForm extends IceForm {
constructor(props) {
super(props);
this.onChange = props.onChange;
let steps = this.props.fields.map((item) => ({
...item,
ref: React.createRef(),
}));
steps = steps.map((item) => {
const { ref, fields } = item;
item.content = (
<IceForm
ref={ref}
adapter={props.adapter}
fields={fields}
twoColumnLayout={props.twoColumnLayout}
width={props.width}
layout={props.layout || 'horizontal'}
/>
);
return item;
});
this.state = {
current: 0,
steps,
loading: false,
};
}
moveToStep(current) {
this.setState({ current });
}
next() {
if (this.validateFields(false) === false) {
return;
}
this.showError(false);
const current = this.state.current + 1;
this.setState({ current });
}
prev() {
const current = this.state.current - 1;
if (current < 0) {
return;
}
this.setState({ current });
}
render() {
const { adapter } = this.props;
const { current, steps } = this.state;
return (
<>
<Steps current={current}>
{steps.map((item, index) => (
<Step key={item.title} title={item.title} onClick={() => this.moveToStep(index)} />
))}
</Steps>
<Divider />
<div className="steps-content">
{steps.map((item, index) => (
<div style={{ display: index === current ? 'block' : 'none' }}>
{item.content}
</div>
))}
</div>
<Divider />
<div className="steps-action">
<Row gutter={16}>
<Col className="gutter-row" span={12} style={{}}>
<Space>
{current < steps.length - 1 && (
<Button type="primary" onClick={() => this.next()}>
{adapter.gt('Next')}
</Button>
)}
{current > 0 && (
<Button onClick={() => this.prev()}>
{adapter.gt('Previous')}
</Button>
)}
</Space>
</Col>
<Col className="gutter-row" span={12} style={{ textAlign: 'right' }}>
<Space>
<Button onClick={() => this.props.closeModal()}>
{adapter.gt('Cancel')}
</Button>
<Button type="primary" loading={this.state.loading} onClick={() => this.saveData()}>
{adapter.gt('Save')}
</Button>
</Space>
</Col>
</Row>
</div>
</>
);
}
async saveData() {
this.setState({ loading: true });
const data = await this.validateFields(true);
if (data) {
this.save(data, () => this.props.closeModal());
}
this.setState({ loading: false });
}
save(params, success) {
const { adapter } = this.props;
adapter.add(params, [], () => adapter.get([]), () => {
this.resetFields();
this.showError(false);
success();
});
}
updateFields(data) {
this.state.steps.forEach((item) => {
const subData = {};
item.fields.forEach(([key]) => {
subData[key] = data[key];
});
this.updateFieldsSubForm(item.ref, item.fields, subData);
});
}
updateFieldsSubForm(ref, fields, data) {
data = this.dataToFormFields(data, fields);
ref.current.resetFields();
if (data == null) {
return;
}
try {
ref.current.setFieldsValue(data);
} catch (e) {
console.log(e);
}
}
async validateFields(all) {
const { adapter } = this.props;
const steps = all ? this.state.steps : this.state.steps.slice(0, this.state.current + 1);
const promiseList = steps.map(
(item) => item.ref.current.validateFields()
.then((values) => {
if (!item.ref.current.isValid()) {
return false;
}
return values;
})
.catch(() => false),
);
const allData = await Promise.all(promiseList);
const failedIndex = allData.findIndex((item) => item === false);
if (failedIndex >= 0) {
this.setState({ current: failedIndex });
return false;
}
let values = Object.assign({}, ...allData);
values = adapter.forceInjectValuesBeforeSave(values);
const msg = adapter.doCustomValidation(values);
if (msg !== null) {
this.showError(msg);
return false;
}
if (adapter.csrfRequired) {
values.csrf = $(`#${adapter.getTableName()}Form`).data('csrf');
}
const id = (adapter.currentElement != null) ? adapter.currentElement.id : null;
if (id != null && id !== '') {
values.id = id;
}
const fields = [].concat.apply([], this.state.steps.map((item) => item.fields));
return this.formFieldsToData(values, fields);
}
getSubFormData(ref, fields, params) {
const { adapter } = this.props;
let values = params;
values = adapter.forceInjectValuesBeforeSave(values);
const msg = adapter.doCustomValidation(values);
if (msg !== null) {
ref.current.showError(msg);
return;
}
if (adapter.csrfRequired) {
values.csrf = $(`#${adapter.getTableName()}Form`).data('csrf');
}
const id = (adapter.currentElement != null) ? adapter.currentElement.id : null;
if (id != null && id !== '') {
values.id = id;
}
return this.formFieldsToData(values, fields);
}
showError(errorMsg) {
this.state.steps.forEach((item) => item.ref.current.showError(errorMsg));
}
resetFields() {
this.state.steps.forEach((item) => item.ref.current.resetFields());
}
hideError() {
this.state.steps.forEach((item) => item.ref.current.hideError());
}
isReady() {
return this.state.steps.reduce((acc, item) => acc && item.ref.current != null, true);
}
}
export default IceStepForm;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { Modal } from 'antd';
import IceFormModal from './IceFormModal';
import IceStepForm from './IceStepForm';
class IceStepFormModal extends IceFormModal {
constructor(props) {
super(props);
this.width = 850;
}
show(data) {
if (!data) {
this.setState({ visible: true });
if (this.iceFormReference.current) {
this.iceFormReference.current.resetFields();
}
} else {
this.setState({ visible: true });
if (this.iceFormReference.current && this.iceFormReference.current.isReady()) {
this.iceFormReference.current.moveToStep(0);
this.iceFormReference.current.updateFields(data);
} else {
this.waitForIt(
() => this.iceFormReference.current && this.iceFormReference.current.isReady(),
() => {
this.iceFormReference.current.updateFields(data);
this.iceFormReference.current.moveToStep(0);
},
1000,
);
}
}
}
hide() {
this.iceFormReference.current.moveToStep(0);
this.setState({ visible: false });
}
render() {
const { fields, adapter } = this.props;
const { width, twoColumnLayout, layout } = this.props.adapter.getFormOptions();
return (
<Modal
visible={this.state.visible}
title={this.props.adapter.gt(this.props.title || adapter.objectTypeName)}
maskClosable={false}
width={width || this.width}
footer={[]}
onCancel={() => {
this.closeModal();
}}
>
<IceStepForm
ref={this.iceFormReference}
adapter={adapter}
fields={fields}
closeModal={() => { this.closeModal(); }}
twoColumnLayout={twoColumnLayout || false}
layout={layout}
/>
</Modal>
);
}
}
export default IceStepFormModal;

250
web/components/IceTable.js Normal file
View File

@@ -0,0 +1,250 @@
import React, {Component} from 'react';
import {Col, Form, Input, Row, Table, Space, Button, Tag, message} from 'antd';
import {
FilterOutlined,
PlusCircleOutlined,
} from '@ant-design/icons';
const { Search } = Input;
class IceTable extends React.Component {
state = {
data: [],
pagination: {},
loading: true,
fetchConfig: false,
//filter: null,
showLoading: true,
currentElement: null,
fetchCompleted: false,
};
constructor(props) {
super(props);
}
componentDidMount() {
const fetchConfig = {
page: 1,
};
message.config({
top: 40,
});
this.setState({
fetchConfig,
//filter: this.props.adapter.filter,
pagination: { 'pageSize': this.props.reader.pageSize }
});
//this.fetch(fetchConfig);
}
handleTableChange = (pagination, filters, sorter) => {
const pager = { ...this.state.pagination };
const { search } = this.state;
pager.current = pagination.current;
this.setState({
pagination: pager,
});
const fetchConfig = {
limit: pagination.pageSize,
page: pagination.current,
sortField: sorter.field,
sortOrder: sorter.order,
filters: JSON.stringify(filters),
search: search,
};
this.setState({
fetchConfig
});
this.fetch(fetchConfig);
};
reload = () => {
const fetchConfig = this.state.fetchConfig;
if (fetchConfig) {
this.fetch(fetchConfig)
}
};
search = (value) => {
console.log('search table:' + value);
this.setState({ search: value });
const fetchConfig = this.state.fetchConfig;
console.log(fetchConfig);
if (fetchConfig) {
fetchConfig.search = value;
this.setState({
fetchConfig
});
this.fetch(fetchConfig)
}
}
addNew = () => {
this.props.adapter.renderForm();
}
showFilters = () => {
this.props.adapter.showFilters();
}
setFilterData = (filter) => {
this.setState({
filter,
});
}
setCurrentElement = (currentElement) => {
this.setState({currentElement});
}
setLoading(value) {
this.setState({ loading: value });
}
fetch = (params = {}) => {
console.log('params:', params);
//this.setState({ loading: this.state.showLoading });
this.setState({ loading: true });
//const hideMessage = message.loading({ content: 'Loading Latest Data ...', key: 'loadingTable', duration: 1});
const pagination = { ...this.state.pagination };
console.log('pagination:', pagination);
if (this.props.adapter.localStorageEnabled) {
try {
const cachedResponse = this.props.reader.getCachedResponse(params);
if (cachedResponse.items) {
this.setState({
loading: false,
data: cachedResponse.items,
pagination,
showLoading: false,
});
} else {
this.props.reader.clearCachedResponse(params);
}
} catch (e) {
this.props.reader.clearCachedResponse(params);
}
}
this.props.reader.get(params)
.then(data => {
// Read total count from server
// pagination.total = data.totalCount;
pagination.total = data.total;
//hideMessage();
// setTimeout(
// () => message.success({ content: 'Loading Completed!', key: 'loadingSuccess', duration: 1 }),
// 600
// );
this.setState({
loading: false,
data: data.items,
pagination,
showLoading: false,
fetchCompleted: true,
});
});
};
getChildrenWithProps(element) {
const childrenWithProps = React.Children.map(this.props.children, child => {
// checking isValidElement is the safe way and avoids a typescript error too
const props = {
element,
adapter: this.props.adapter,
loading: this.state.loading,
};
if (React.isValidElement(child)) {
return React.cloneElement(child, props);
}
return child;
});
return childrenWithProps;
}
render() {
return (
<Row direction="vertical" style={{ width: '100%' }}>
{!this.state.currentElement &&
<Col span={24}>
<Row gutter={24}>
<Col span={18}>
<Space>
{this.props.adapter.hasAccess('save') && this.props.adapter.getShowAddNew() &&
<Button type="primary" onClick={this.addNew}><PlusCircleOutlined/> Add New</Button>
}
{this.props.adapter.getFilters() &&
<Button onClick={this.showFilters}><FilterOutlined/> Filters</Button>
}
{this.state.fetchCompleted
&& this.props.adapter.getFilters()
&& this.props.adapter.filter != null
&& this.props.adapter.filter !== []
&& this.props.adapter.filter !== ''
&& this.props.adapter.getFilterString(this.props.adapter.filter) !== '' &&
<Tag closable
style={{'lineHeight': '30px'}}
color="blue"
onClose={() => this.props.adapter.resetFilters()}
visible={this.props.adapter.filter != null && this.props.adapter.filter !== [] && this.props.adapter.filter !== ''}
>
{this.props.adapter.getFilterString(this.props.adapter.filter)}
</Tag>
}
</Space>
</Col>
<Col span={6}>
<Form
ref={(formRef) => this.form = formRef}
name="advanced_search"
className="ant-advanced-search-form"
>
<Form.Item name="searchTerm" label=""
rules={[
{
required: false,
},
]}
>
<Search
placeholder="input search text"
enterButton="Search"
onSearch={value => this.search(value)}
/>
</Form.Item>
</Form>
</Col>
</Row>
<Row gutter={24}>
<Col span={24}>
<Table
// bordered
rowClassName={(record, index) => index % 2 === 0 ? 'table-row-light' : 'table-row-dark'}
columns={this.props.columns}
rowKey={record => record.id}
dataSource={this.state.data}
pagination={this.state.pagination}
loading={this.state.loading}
onChange={this.handleTableChange}
reader={this.props.dataPipe}
/>
</Col>
</Row>
</Col>
}
{this.state.currentElement &&
this.getChildrenWithProps(this.state.currentElement)
}
</Row>
);
}
}
export default IceTable;

157
web/components/IceUpload.js Normal file
View File

@@ -0,0 +1,157 @@
import React from "react";
import {Button, message, Space, Upload, Tag} from "antd";
import { UploadOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons';
class IceUpload extends React.Component {
state = {
fileList: [],
uploaded: false,
};
_isMounted = false;
constructor(props) {
super(props);
this.onChange = props.onChange;
}
componentDidMount() {
this._isMounted = true;
message.config({
top: 55,
duration: 2,
});
}
componentWillUnmount() {
this._isMounted = false;
}
handleDelete = () => {
this.setState({ fileList: [], value: null, uploaded: false});
this.onChange(null);
};
handleView = () => {
let currentValue = this.props.value;
if (this.state.value != null && this.state.value !== '') {
currentValue = this.state.value;
}
if (currentValue == null || currentValue === '') {
message.error('File not found');
return;
}
const { adapter } = this.props;
adapter.getFile(currentValue)
.then((data) => {
const file = {
key: data.uid,
uid: data.uid,
name: data.name,
status: data.status,
url: data.filename,
};
window.open(file.url);
}).catch((e) => {
});
};
handleChange = info => {
let fileList = [...info.fileList];
if (fileList.length === 0) {
this.setState({ value: null });
this.onChange(null);
this.setState({fileList: []});
this.setState({uploaded: false});
return;
}
fileList = fileList.slice(-1);
if (fileList[0].response && fileList[0].response.status === 'error') {
this.setState({ value: null });
this.onChange(null);
this.setState({fileList: []});
this.setState({uploaded: false});
message.error(`Error: ${fileList[0].response.message}`);
return;
}
fileList = fileList.map(file => {
if (file.response) {
// Component will show file.url as link
file.name = file.response.name;
file.url = file.response.url;
}
return file;
});
this.setState({fileList});
this.setState({ value: this.getFileName(fileList), uploaded: true });
this.onChange(this.getFileName(fileList));
};
getFileName(fileList) {
let file = null;
if (fileList) {
file = fileList[0];
}
return file ? file.name : '';
}
generateRandom(length) {
const d = new Date();
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = length; i > 0; --i) result += chars[Math.round(Math.random() * (chars.length - 1))];
return result + d.getTime();
}
render() {
let fileName = this.generateRandom(14);
const props = {
action: `${window.CLIENT_BASE_URL}fileupload-new.php?user=${this.props.user}&file_group=${this.props.fileGroup}&file_name=${fileName}`,
onChange: this.handleChange,
onRemove: this.handleDelete,
multiple: false,
listType: 'picture',
};
return (
<Space direction={'vertical'}>
{!this.props.readOnly &&
<Space>
<Upload {...props} fileList={this.state.fileList}>
<Tag color="blue" style={{ cursor: 'pointer' }}>
<UploadOutlined />
{' '}
Upload
</Tag>
</Upload>
</Space>
}
<Space>
{ (((this.props.value != null && this.props.value !== '') || (this.state.value != null && this.state.value !== '')) && !this.state.uploaded) &&
<Button type="link" htmlType="button" onClick={this.handleView}>
<DownloadOutlined/> View File
</Button>
}
{ (((this.props.value != null && this.props.value !== '') || (this.state.value != null && this.state.value !== '')) && !this.state.uploaded && !this.props.readOnly) &&
<Button type="link" htmlType="button" danger onClick={this.handleDelete}>
<DeleteOutlined/> Delete
</Button>
}
</Space>
</Space>
);
}
}
export default IceUpload;

55
web/components/TagList.js Normal file
View File

@@ -0,0 +1,55 @@
import React from 'react';
import {Skeleton, Tag} from 'antd';
class TagList extends React.Component {
state = {
tags: [],
loading: true,
}
constructor(props) {
super(props);
}
componentDidMount() {
this.fetch();
}
fetch() {
this.setState({
loading: true,
});
this.props.apiClient
.get(this.props.url)
.then((response) => {
const tags = response.data.data.map(this.props.extractTag);
this.setState({
tags: tags,
loading: false,
});
});
}
render() {
return (
<div style={{
display: 'inline-block',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
width: '100%',
}}>
{this.state.loading &&
<Skeleton active={true}/>
}
{!this.state.loading && this.state.tags.map((tag, index) =>
this.props.render ? this.props.render(tag) : <div key={`p${index}`}><Tag color={this.props.color} key={index} style={{margin: '10px'}}>{tag}</Tag><br/></div>
)}
</div>
);
}
}
export default TagList;

187
web/components/TaskList.js Normal file
View File

@@ -0,0 +1,187 @@
import React from 'react';
import {
Timeline, Drawer, Empty, Button, Space, Typography, Popover,
} from 'antd';
import {
ClockCircleOutlined,
PlusCircleOutlined,
InfoCircleOutlined,
PauseCircleOutlined,
MedicineBoxOutlined,
} from '@ant-design/icons';
const { Paragraph } = Typography;
class TaskList extends React.Component {
state = {
tasks: [],
showAll: false,
}
constructor(props) {
super(props);
this.state.tasks = this.props.tasks.map(item => false);
}
render() {
return this.createTaskList(4);
}
createTaskList(maxNumberOfTasks) {
const tasks = this.props.tasks.slice(0, maxNumberOfTasks);
return (
<>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{this.props.tasks && this.props.tasks.length > 0
&& (
<Space direction="vertical" style={{ width: '100%' }}>
<Timeline style={{ width: '100%' }}>
{tasks.map(
(task, index) => (
this.createTask(task, index)
),
)}
</Timeline>
{this.props.tasks.length > maxNumberOfTasks &&
<Button type="primary" onClick={() => this.showAllTasks()}>
View All
{' '}
{this.props.tasks.length}
{' '}
Tasks
</Button>
}
</Space>
)}
{this.props.tasks && this.props.tasks.length === 0
&& <Empty description="You're all caught up!" />}
</Space>
<Drawer
title="Task List"
width={470}
onClose={() => this.hideAllTasks()}
visible={this.state.showAll}
bodyStyle={{ paddingBottom: 80 }}
zIndex={1200}
maskClosable={false}
>
<Timeline style={{ width: '100%' }}>
{this.props.tasks.map(
(task, index) => (
this.createTask(task, index)
),
)}
</Timeline>
</Drawer>
</>
);
}
visitLink(link) {
setTimeout(() => {window.open(link);}, 100);
}
handleTaskHover(index) {
this.setState({tasks : this.props.tasks.map((item,i) => index === i)})
}
createTask(task, index) {
if (task.priority === 100) {
return (
<Timeline.Item onMouseEnter={() => this.handleTaskHover(index)}
dot={<ClockCircleOutlined style={{ fontSize: '16px' }} />} color="red" >
{this.getText(task)}
{task.link && this.state.tasks[index]
&& (
<Button type="link" onClick={() => this.visitLink(task.link)}>
<MedicineBoxOutlined style={{ fontSize: '16px' }} />
{' '}
{task.action}
</Button>
)}
</Timeline.Item>
);
} if (task.priority === 50) {
return (
<Timeline.Item onMouseEnter={() => this.handleTaskHover(index)}
dot={<InfoCircleOutlined style={{ fontSize: '16px' }} />} color="blue">
{this.getText(task)}
{task.link && this.state.tasks[index]
&& (
<Button type="link" onClick={() => this.visitLink(task.link)}>
<MedicineBoxOutlined style={{ fontSize: '16px' }} />
{' '}
{task.action}
</Button>
)}
</Timeline.Item>
);
} if (task.priority === 20) {
return (
<Timeline.Item onMouseEnter={() => this.handleTaskHover(index)}
dot={<PlusCircleOutlined style={{ fontSize: '16px' }} />} color="blue">
{this.getText(task)}
{task.link && this.state.tasks[index]
&& (
<Button type="link" onClick={() => this.visitLink(task.link)}>
<MedicineBoxOutlined style={{ fontSize: '16px' }} />
{' '}
{task.action}
</Button>
)}
</Timeline.Item>
);
} if (task.priority === 10) {
return (
<Timeline.Item onMouseEnter={() => this.handleTaskHover(index)}
dot={<PauseCircleOutlined style={{ fontSize: '16px' }} />} color="green">
{this.getText(task)}
{task.link && this.state.tasks[index]
&& (
<Button type="link" onClick={() => this.visitLink(task.link)}>
<MedicineBoxOutlined style={{ fontSize: '16px' }} />
{' '}
{task.action}
</Button>
)}
</Timeline.Item>
);
}
}
getText(task) {
if (!task.details) {
return (<Paragraph
ellipsis={{
rows: 1,
expandable: true,
}}
>
{task.text}
</Paragraph>);
}
return (
<Popover content={task.details}>
<Paragraph
ellipsis={{
rows: 1,
expandable: true,
}}
>
{task.text}
</Paragraph>
</Popover>
);
}
showAllTasks() {
this.setState({showAll: true});
}
hideAllTasks() {
this.setState({showAll: false});
}
}
export default TaskList;