Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"scripts": {
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('You must use Yarn to install, not NPM')\"",
"clean": "rimraf dist/*",
"upgrade-form-control": "yarn upgrade bahmni-form-controls@0.94.0",
"upgrade-form-control": "yarn upgrade bahmni-form-controls@1.0.2",
"copy": "copyfiles -f ./index.html ./dist",
"build": "yarn run copy && webpack",
"build-dev": "yarn run copy && webpack --config webpack.dev.config.js",
Expand Down Expand Up @@ -81,7 +81,7 @@
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"bahmni-form-controls": "^0.94.0",
"bahmni-form-controls": "^1.0.2",
"classnames": "^2.2.5",
"codemirror": "^5.51.0",
"core-js": "^2.4.1",
Expand Down
1 change: 1 addition & 0 deletions src/form-builder/actions/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ export const formLoad = (controls) => ({ type: 'FORM_LOAD', controls });
export const deleteControl = (controlIds) => ({ type: 'DELETE_CONTROL', controlIds });

export const formDefVersionUpdate = (version) => ({ type: 'FORM_DEFINITION_VERSION_UPDATE', version });
export const setAllowedDomains = (domains) => ({ type: 'SET_ALLOWED_DOMAINS', domains });
2 changes: 1 addition & 1 deletion src/form-builder/components/ControlPropertiesContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class ControlPropertiesContainer extends Component {

displayPropertyEditor() {
const { selectedControl, selectedControl: { id, concept } } = this.props;
Comment thread
vvkpd marked this conversation as resolved.
if (concept || selectedControl.type === 'section') {
if (concept || selectedControl.type === 'section' || selectedControl.type === 'label') {
return (
<PropertyEditor
metadata={selectedControl}
Expand Down
8 changes: 8 additions & 0 deletions src/form-builder/components/ControlReduxWrapper.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,9 @@ export class ControlWrapper extends Draggable {
tabIndex="1"
>
Comment thread
vvkpd marked this conversation as resolved.
<this.control
allowedDomains={this.props.allowedDomains}
clearSelectedControl={ this.clearSelectedControl}
controlProperty={this.props.controlProperty}
deleteControl={ this.confirmDelete }
dispatch={this.clearControlProperties}
dragSourceCell= {this.props.dragSourceCell}
Expand All @@ -232,6 +234,7 @@ export class ControlWrapper extends Draggable {
onControlDrop={this.handleControlDrop}
onSelect={ this.onSelected }
ref={ this.storeChildRef }
selectedControlId={ this.props.selectedControl && this.props.selectedControl.id }
setError={this.props.setError}
showDeleteButton={ this.props.showDeleteButton && this.state.active }
wrapper={ this.props.wrapper }
Expand All @@ -244,6 +247,10 @@ export class ControlWrapper extends Draggable {
}

ControlWrapper.propTypes = {
allowedDomains: PropTypes.arrayOf(PropTypes.string),
selectedControl: PropTypes.shape({
id: PropTypes.string,
}),
controlProperty: PropTypes.shape({
id: PropTypes.string,
property: PropTypes.object,
Expand All @@ -268,6 +275,7 @@ function mapStateToProps(state) {
selectedControl: state.controlDetails.selectedControl,
dragSourceCell: state.controlDetails.dragSourceCell,
allObsControlEvents: state.controlDetails.allObsControlEvents,
allowedDomains: state.formDetails && state.formDetails.allowedDomains,
};
}

Expand Down
9 changes: 9 additions & 0 deletions src/form-builder/components/FormBuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import NotificationContainer from 'common/Notification';
import { remove } from 'lodash';
import Spinner from 'common/Spinner';
import { formEventUpdate, saveEventUpdate } from 'form-builder/actions/control';
import { validateFormHyperlinks } from 'form-builder/helpers/hyperlinkValidationHelper';

export default class FormBuilder extends Component {

Expand Down Expand Up @@ -299,6 +300,13 @@ export default class FormBuilder extends Component {
saveFormJson(form, value, formName, translations, nameTranslations) {
const self = this;
const val = value;
const hyperlinkErrors = validateFormHyperlinks(val, this.props.allowedDomains || []);
if (hyperlinkErrors.length > 0) {
this.props.onValidationError(
`Import failed for form "${formName}": ${hyperlinkErrors.join('; ')}`
);
return Promise.resolve();
}
return httpInterceptor.post(formBuilderConstants.formUrl, form).then((response) => {
val.uuid = response.uuid;
const formResource = {
Expand Down Expand Up @@ -512,6 +520,7 @@ export default class FormBuilder extends Component {
}

FormBuilder.propTypes = {
allowedDomains: PropTypes.arrayOf(PropTypes.string),
data: PropTypes.array.isRequired,
dispatch: PropTypes.func.isRequired,
match: PropTypes.shape({
Expand Down
19 changes: 14 additions & 5 deletions src/form-builder/components/FormBuilderContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import get from 'lodash/get';
import map from 'lodash/map';
import sortBy from 'lodash/sortBy';
import formHelper from '../helpers/formHelper';
import { fetchAllowedDomains } from '../helpers/hyperlinkValidationHelper';
import { connect } from 'react-redux';
import { setDefaultLocale } from '../actions/control';
import { setDefaultLocale, setAllowedDomains } from '../actions/control';
import { saveFormNameTranslations, saveTranslations } from 'common/apis/formTranslationApi';


Expand All @@ -36,6 +37,9 @@ export class FormBuilderContainer extends Component {
this.getFormData().then(() => {
this.getDefaultLocale();
});
fetchAllowedDomains().then((allowedDomains) => {
this.props.dispatch(setAllowedDomains(allowedDomains));
});
}

onValidationError(message) {
Expand Down Expand Up @@ -162,9 +166,7 @@ export class FormBuilderContainer extends Component {
});
self.saveTranslations(updatedTranslations, formNameTranslationsResource);
})
.catch(() => {
this.setMessage('Error Importing Form', commonConstants.responseType.error);
});
.catch((error) => this.showErrors(error));
}

render() {
Expand All @@ -175,6 +177,7 @@ export class FormBuilderContainer extends Component {
notification={this.state.notification}
/>
<FormBuilder
allowedDomains={this.props.allowedDomains}
data={this.state.data}
dispatch={this.props.dispatch}
match={this.props.match}
Expand Down Expand Up @@ -204,4 +207,10 @@ FormBuilderContainer.propTypes = {
routes: PropTypes.array,
};

export default connect()(FormBuilderContainer);
function mapStateToProps(state) {
return {
allowedDomains: state.formDetails && state.formDetails.allowedDomains,
};
}

export default connect(mapStateToProps)(FormBuilderContainer);
27 changes: 25 additions & 2 deletions src/form-builder/components/FormDetailContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
formLoad,
setChangedProperty,
formDefVersionUpdate,
setAllowedDomains,
} from 'form-builder/actions/control';
import NotificationContainer from 'common/Notification';
import Spinner from 'common/Spinner';
Expand All @@ -44,6 +45,7 @@ import {
import FormPreviewModal from 'form-builder/components/FormPreviewModal.jsx';
import Popup from 'reactjs-popup';
import { saveFormPrivileges } from 'common/apis/formPrivilegesApi';
import { validateFormHyperlinks, fetchAllowedDomains } from 'form-builder/helpers/hyperlinkValidationHelper';

export class FormDetailContainer extends Component {
constructor(props) {
Expand Down Expand Up @@ -123,6 +125,11 @@ export class FormDetailContainer extends Component {
// .then is untested

this.getFormList();
Comment thread
vvkpd marked this conversation as resolved.
if (!this.props.allowedDomains) {
fetchAllowedDomains().then((allowedDomains) => {
this.props.dispatch(setAllowedDomains(allowedDomains));
});
}
}

componentWillUpdate(nextProps, nextState) {
Expand Down Expand Up @@ -183,6 +190,14 @@ export class FormDetailContainer extends Component {

onSave() {
try {
const formResource = this.getFormResource();
const hyperlinkErrors = validateFormHyperlinks(
JSON.parse(formResource.value), this.props.allowedDomains || []
);
if (hyperlinkErrors.length > 0) {
this.setErrorMessage({ message: hyperlinkErrors.join('; ') });
return;
}
const initialPrivileges = [];
const formId = this.state.formData.id;
const formVersion = this.state.formData.version;
Expand All @@ -192,7 +207,6 @@ export class FormDetailContainer extends Component {
initialPrivileges.push(privilege);
});
this.setState({ formPrivileges: initialPrivileges, loading: false });
const formResource = this.getFormResource();
this._saveFormResource(formResource);
this._saveFormPrivileges(this.state.formData.id,
this.state.formData.version, this.state.formPrivileges);
Expand Down Expand Up @@ -246,6 +260,14 @@ export class FormDetailContainer extends Component {

onPublish() {
try {
const formJson = this.getFormResource();
const hyperlinkErrors = validateFormHyperlinks(
JSON.parse(formJson.value), this.props.allowedDomains || []
);
if (hyperlinkErrors.length > 0) {
this.setErrorMessage({ message: hyperlinkErrors.join('; ') });
return;
}
const initialPrivileges = [];
const formId = this.state.formData.id;
const formVersion = this.state.formData.version;
Expand All @@ -255,7 +277,6 @@ export class FormDetailContainer extends Component {
initialPrivileges.push(privilege);
});
this.setState({ formPrivileges: initialPrivileges, loading: false });
const formJson = this.getFormResource();
httpInterceptor.post(formBuilderConstants.bahmniFormResourceUrl, formJson)
.then((response) => {
this.setFormData(response);
Expand Down Expand Up @@ -515,6 +536,7 @@ export class FormDetailContainer extends Component {
position="top center"
>
<FormPreviewModal
allowedDomains={this.props.allowedDomains}
close={() => this.closePreview()}
formJson={this.state.formPreviewJson}
setErrorMessage={this.setErrorMessage}
Expand Down Expand Up @@ -777,6 +799,7 @@ FormDetailContainer.contextTypes = {
function mapStateToProps(state) {
return {
defaultLocale: state.formDetails && state.formDetails.defaultLocale,
allowedDomains: state.formDetails && state.formDetails.allowedDomains,
translations: state.translations,
formDetails: state.formDetails,
formControlEvents: state.controlDetails.allObsControlEvents,
Expand Down
5 changes: 4 additions & 1 deletion src/form-builder/components/FormPreviewModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ export default class FormPreviewModal extends React.Component {
const container = React.createElement(Container,
{ metadata, observations, validate: true, validateForm: false,
collapse: false, patient: null, locale: this.state.defaultLocale, translations: '',
onValueUpdated: this.onValueUpdated });
onValueUpdated: this.onValueUpdated,
allowedDomains: this.props.allowedDomains || [],
showValidationErrors: true });
ReactDOM.render(container, document.getElementById('form-container'));
}
}
Expand Down Expand Up @@ -100,6 +102,7 @@ export default class FormPreviewModal extends React.Component {
}

FormPreviewModal.propTypes = {
allowedDomains: PropTypes.arrayOf(PropTypes.string),
close: PropTypes.func.isRequired,
formJson: PropTypes.object,
setErrorMessage: PropTypes.func.isRequired,
Expand Down
2 changes: 1 addition & 1 deletion src/form-builder/components/FormPrivilegeTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export default class FormPrivilegeTable extends Component {
}

fetchPrivileges() {
let initialPrivileges = [];
const initialPrivileges = [];
let privileges = [];
const queryParams = '?=';
const optionsUrl = `${formBuilderConstants.formPrivilegeUrl}${queryParams}`;
Expand Down
6 changes: 3 additions & 3 deletions src/form-builder/components/Property.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { formBuilderConstants } from 'form-builder/constants';

export class Property extends Component {

Expand Down Expand Up @@ -43,10 +44,9 @@ export class Property extends Component {
</select>);
case 'text':
return (<input
className="fr"
defaultValue={this.props.value}
key={`${this.props.name}:${this.props.id}`}
{...(this.props.name === 'url'
{...(this.props.name === 'url' || this.props.name === formBuilderConstants.hyperlinkUrlProperty
? { onBlur: e => this.updateProperty(e, elementType) }
: { onChange: e => this.updateProperty(e, elementType) })}
type="text"
Expand All @@ -73,7 +73,7 @@ export class Property extends Component {
render() {
const { name, elementType } = this.props;
return (
<div>
<div className={elementType === 'text' ? 'property-text-row' : undefined}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This applies property-text-row (and the removal of className="fr" above) to every text-type property — including the existing url property and any future text property on any control, not just hyperlinkUrl. That's a broader layout change than the PR scope and risks a visual regression for the existing url field.

Consider scoping the class (e.g. key off name === formBuilderConstants.hyperlinkUrlProperty) and preserving the previous styling for other text fields.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. The property-text-row layout applies to all text-type properties — including the existing url field on CodedControl — since text inputs benefit from the same layout treatment. This is a deliberate improvement across all text properties in the property panel, not scoped to hyperlinkUrl alone.

<label>{name}</label>
{this.getElement(elementType)}
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/form-builder/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ export const formBuilderConstants = {
pdfDownloadUrl: '/openmrs/ws/rest/v1/bahmniie/form/download/',
dataLimit: 9999,
formDefinitionVersion: 2.0,
hyperlinkUrlProperty: 'hyperlinkUrl',
allowedDomainsGPUrl: '/openmrs/ws/rest/v1/bahmnicore/sql/globalproperty?property=bahmni.forms.hyperlink.allowedDomains',

};
32 changes: 32 additions & 0 deletions src/form-builder/helpers/hyperlinkValidationHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { validateHyperlink } from 'bahmni-form-controls';
Comment thread
vvkpd marked this conversation as resolved.
import { httpInterceptor } from 'common/utils/httpInterceptor';
import { formBuilderConstants } from 'form-builder/constants';

function collectHyperlinkUrls(controls, acc, visited = new Set()) {
if (!controls) return acc;
controls.forEach((control) => {
if (visited.has(control)) return;
visited.add(control);
if (control.type === 'label' && control.properties && control.properties.hyperlinkUrl) {
acc.push(control.properties.hyperlinkUrl);
}
if (control.label) collectHyperlinkUrls([control.label], acc, visited);
if (control.controls) collectHyperlinkUrls(control.controls, acc, visited);
});
return acc;
}

export function validateFormHyperlinks(formJson, allowedDomains) {
const urls = collectHyperlinkUrls(formJson.controls || [], []);
return urls
.map((url) => validateHyperlink(url, allowedDomains))
.filter((result) => !result.valid)
.map((result) => `Invalid hyperlink: ${result.error}`);
}

export function fetchAllowedDomains() {
return httpInterceptor
.get(formBuilderConstants.allowedDomainsGPUrl, 'text')
.then((data) => (data || '').split(',').map((d) => d.trim()).filter(Boolean))
.catch(() => []);
Comment thread
vvkpd marked this conversation as resolved.
}
Comment thread
vvkpd marked this conversation as resolved.
2 changes: 2 additions & 0 deletions src/form-builder/reducers/formDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const formDetails = (store = {}, action) => {
return Object.assign({}, store, { defaultLocale: action.locale });
case 'FORM_DEFINITION_VERSION_UPDATE':
return Object.assign({}, store, { formDefVersion: action.version });
case 'SET_ALLOWED_DOMAINS':
return Object.assign({}, store, { allowedDomains: action.domains });
default:
return store;
}
Expand Down
27 changes: 27 additions & 0 deletions styles/common/_canvas.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,30 @@
min-height: 40px;
}
}

[data-bahmni-hyperlink],
.form-builder-hyperlink-preview {
text-decoration: none;

&:hover {
text-decoration: underline;
}
}

.obs-attached-label {
display: block;
text-align: right;
min-height: 23px;

.control-wrapper-content {
display: inline-flex;
align-items: center;
}

.remove-control-button {
position: static;
margin-left: 4px;
top: auto;
right: auto;
}
}
Loading