diff --git a/src/common/apis/formPrivilegesApi.js b/src/common/apis/formPrivilegesApi.js index 1c780d3d..61d82e1b 100644 --- a/src/common/apis/formPrivilegesApi.js +++ b/src/common/apis/formPrivilegesApi.js @@ -14,6 +14,15 @@ import { UrlHelper } from 'form-builder/helpers/UrlHelper'; export function saveFormPrivileges(formPrivileges) { return httpInterceptor.post(formBuilderConstants.saveFormPrivilegesUrl, formPrivileges); } +export function buildFormPrivilegesPayload(formId, formVersion, formPrivileges) { + return formPrivileges.map((privilege) => ({ + formId, + formVersion, + privilegeName: privilege.privilegeName, + editable: privilege.editable, + viewable: privilege.viewable, + })); +} export function getFormPrivileges(formId, formVersion) { return httpInterceptor.get(new UrlHelper() .getFormPrivilegesUrl(formId, formVersion), 'text'); diff --git a/src/form-builder/components/FormBuilder.jsx b/src/form-builder/components/FormBuilder.jsx index a8700e4f..d50b2e5a 100644 --- a/src/form-builder/components/FormBuilder.jsx +++ b/src/form-builder/components/FormBuilder.jsx @@ -26,6 +26,9 @@ import { remove } from 'lodash'; import Spinner from 'common/Spinner'; import { formEventUpdate, saveEventUpdate } from 'form-builder/actions/control'; import { validateFormHyperlinks } from 'form-builder/helpers/hyperlinkValidationHelper'; +import { + saveFormPrivileges, getFormPrivilegesFromUuid, buildFormPrivilegesPayload, +} from 'common/apis/formPrivilegesApi'; export default class FormBuilder extends Component { @@ -62,7 +65,8 @@ export default class FormBuilder extends Component { const version = this.getFormVersion(formName); let uuid = ''; this.props.data.forEach(form => { - if (form.name === formName && form.version === version) { + // eslint-disable-next-line + if (form.name === formName && parseInt(form.version) === version) { uuid = form.uuid; } }); @@ -238,6 +242,7 @@ export default class FormBuilder extends Component { const formName = formJson.name; const value = JSON.parse(formJson.resources[0].value); const nameTranslations = formJson.resources[1] && formJson.resources[1].value; + const privileges = formJson.privileges || []; const form = { name: formName, version: '1', @@ -256,7 +261,9 @@ export default class FormBuilder extends Component { }); self.updateImportErrors(fileName, message); } else { - self.formJSONs.push({ form, value, formName, translations, nameTranslations }); + self.formJSONs.push({ + form, value, formName, translations, nameTranslations, privileges, + }); } }); } @@ -288,16 +295,28 @@ export default class FormBuilder extends Component { const self = this; const importFormJsonPromises = []; formJsons.forEach(formJson => { - const { form, value, formName, translations, nameTranslations } = formJson; + const { form, value, formName, translations, nameTranslations, privileges } = formJson; importFormJsonPromises.push(self.saveFormJson(form, value, formName, translations, - nameTranslations)); + nameTranslations, privileges)); }); Promise.all(importFormJsonPromises) - .then(() => self.hideLoader()) + .then(() => { + if (self.props.onImportComplete) { + self.props.onImportComplete(); + } + self.hideLoader(); + }) .catch(() => self.hideLoader()); } - saveFormJson(form, value, formName, translations, nameTranslations) { + saveImportedFormPrivileges(formId, formVersion, privileges) { + if (!privileges || privileges.length === 0) { + return Promise.resolve(); + } + return saveFormPrivileges(buildFormPrivilegesPayload(formId, formVersion, privileges)); + } + + saveFormJson(form, value, formName, translations, nameTranslations, privileges) { const self = this; const val = value; const hyperlinkErrors = validateFormHyperlinks(val, this.props.allowedDomains || []); @@ -327,8 +346,15 @@ export default class FormBuilder extends Component { }; const translationsWithFormUuid = translations.map((eachTranslation) => Object.assign({}, eachTranslation, { formUuid: response.uuid })); - self.props.saveFormResource(formResource, translationsWithFormUuid, - formNameTranslationsResource); + return self.props.saveFormResource(formResource, translationsWithFormUuid, + formNameTranslationsResource) + .then((savedForm) => + self.saveImportedFormPrivileges(savedForm.id, savedForm.version, privileges)) + .catch(() => { + self.props.onValidationError( + `Import failed for form "${formName}": could not save form content` + ); + }); }) .catch(() => { const formUuid = self.getFormUuid(formName); @@ -336,7 +362,7 @@ export default class FormBuilder extends Component { const params = 'v=custom:(id,uuid,name,version,published,auditInfo,' + 'resources:(value,dataType,uuid))'; - httpInterceptor.get(`${formBuilderConstants.formUrl}/${formUuid}?${params}`) + return httpInterceptor.get(`${formBuilderConstants.formUrl}/${formUuid}?${params}`) .then((data) => { const formResource = { form: { @@ -354,7 +380,15 @@ export default class FormBuilder extends Component { value: nameTranslations, uuid: '', }; - self.props.saveFormResource(formResource, translations, formNameTranslationsResource); + return self.props + .saveFormResource(formResource, translations, formNameTranslationsResource) + .then((savedForm) => + self.saveImportedFormPrivileges(savedForm.id, savedForm.version, privileges)); + }) + .catch(() => { + self.props.onValidationError( + `Import failed for form "${formName}": could not resolve the existing form` + ); }); }); } @@ -436,7 +470,6 @@ export default class FormBuilder extends Component { return; } const zip = new JSZip(); - let fileName; let params = ''; const uuids = this.state.selectedForms; uuids.forEach((uuid, index) => { @@ -450,19 +483,27 @@ export default class FormBuilder extends Component { commonConstants.responseType.error); } const formData = exportResponse.bahmniFormDataList; - formData.forEach(form => { - fileName = `${form.formJson.name}_${form.formJson.version}`; - zip.file(`${fileName}.json`, JSON.stringify(form)); - }); - if (formData.length > 0) { - zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }).then((content) => { - saveAs(content, commonConstants.exportFileName); + const privilegesPromises = formData.map((form) => + getFormPrivilegesFromUuid(form.formJson.uuid).catch(() => [])); + return Promise.all(privilegesPromises).then((privilegesList) => { + formData.forEach((form, index) => { + const fileName = `${form.formJson.name}_${form.formJson.version}`; + const formWithPrivileges = Object.assign({}, form, { + formJson: Object.assign({}, form.formJson, + { privileges: privilegesList[index] }), + }); + zip.file(`${fileName}.json`, JSON.stringify(formWithPrivileges)); }); - if (exportResponse.errorFormList.length === 0) { - this.setMessage(commonConstants.exportFormsSuccessMessage, - commonConstants.responseType.success); + if (formData.length > 0) { + zip.generateAsync({ type: 'blob', compression: 'DEFLATE' }).then((content) => { + saveAs(content, commonConstants.exportFileName); + }); + if (exportResponse.errorFormList.length === 0) { + this.setMessage(commonConstants.exportFormsSuccessMessage, + commonConstants.responseType.success); + } } - } + }); }) .catch(() => { this.setMessage('Export failed', commonConstants.responseType.error); @@ -529,6 +570,7 @@ FormBuilder.propTypes = { isExact: PropTypes.bool.isRequired, params: PropTypes.object, }), + onImportComplete: PropTypes.func, onValidationError: PropTypes.func, routes: PropTypes.array, saveForm: PropTypes.func.isRequired, diff --git a/src/form-builder/components/FormBuilderContainer.jsx b/src/form-builder/components/FormBuilderContainer.jsx index fd88f16c..96763810 100644 --- a/src/form-builder/components/FormBuilderContainer.jsx +++ b/src/form-builder/components/FormBuilderContainer.jsx @@ -48,8 +48,8 @@ export class FormBuilderContainer extends Component { } getFormData() { - let initialForms = []; - let forms = []; + const initialForms = []; + const forms = []; const queryParams = '?='; const fetchFormsUrl = `${formBuilderConstants.formUrl}?v=custom:(id,uuid,name,version,published,auditInfo)`; return httpInterceptor.get(fetchFormsUrl) @@ -156,7 +156,7 @@ export class FormBuilderContainer extends Component { saveFormResource(formJson, formTranslations, formNameTranslationsResource) { const self = this; self.setMessage('Importing Form...', commonConstants.responseType.success); - httpInterceptor.post(formBuilderConstants.bahmniFormResourceUrl, formJson) + return httpInterceptor.post(formBuilderConstants.bahmniFormResourceUrl, formJson) .then((form) => { const updatedTranslations = map(formTranslations, (translation) => { const formTranslation = translation; @@ -165,8 +165,8 @@ export class FormBuilderContainer extends Component { return formTranslation; }); self.saveTranslations(updatedTranslations, formNameTranslationsResource); - }) - .catch((error) => this.showErrors(error)); + return form.form; + }); } render() { @@ -181,6 +181,7 @@ export class FormBuilderContainer extends Component { data={this.state.data} dispatch={this.props.dispatch} match={this.props.match} + onImportComplete={() => this.getFormData()} onValidationError={(messages) => this.onValidationError(messages)} routes={this.props.routes} saveForm={(formName) => this.saveForm(formName)} diff --git a/src/form-builder/components/FormDetailContainer.jsx b/src/form-builder/components/FormDetailContainer.jsx index 596eaed1..ba155f2c 100644 --- a/src/form-builder/components/FormDetailContainer.jsx +++ b/src/form-builder/components/FormDetailContainer.jsx @@ -44,7 +44,7 @@ import { } from 'common/apis/formTranslationApi'; import FormPreviewModal from 'form-builder/components/FormPreviewModal.jsx'; import Popup from 'reactjs-popup'; -import { saveFormPrivileges } from 'common/apis/formPrivilegesApi'; +import { saveFormPrivileges, buildFormPrivilegesPayload } from 'common/apis/formPrivilegesApi'; import { validateFormHyperlinks, fetchAllowedDomains } from 'form-builder/helpers/hyperlinkValidationHelper'; export class FormDetailContainer extends Component { @@ -227,9 +227,8 @@ export class FormDetailContainer extends Component { }); } _saveFormPrivileges(formId, formVersion) { - let formVersionTemp = formVersion; saveFormPrivileges( - this._createReqObject(formId, formVersionTemp, this.state.formPrivileges) + buildFormPrivilegesPayload(formId, formVersion, this.state.formPrivileges) ) .then(() => { const message = 'Form Privileges saved successfully'; @@ -242,22 +241,6 @@ export class FormDetailContainer extends Component { }); } - _createReqObject(formId, formVersion, formPrivileges) { - const formPrivilegeObj = []; - for (let i = 0; i < formPrivileges.length; i++) { - const privilege = formPrivileges[i]; - const privilegeCopy = { - formId, - privilegeName: privilege.privilegeName, - editable: privilege.editable, - viewable: privilege.viewable, - formVersion, - }; - formPrivilegeObj.push(privilegeCopy); - } - return formPrivilegeObj; - } - onPublish() { try { const formJson = this.getFormResource(); diff --git a/test/form-builder/components/FormBuilder.spec.js b/test/form-builder/components/FormBuilder.spec.js index 2d5c749e..74315f59 100644 --- a/test/form-builder/components/FormBuilder.spec.js +++ b/test/form-builder/components/FormBuilder.spec.js @@ -21,6 +21,7 @@ import jsonpath from 'jsonpath/jsonpath'; import * as FormBuilderBreadcrumbs from 'form-builder/components/FormBuilderBreadcrumbs.jsx'; import { MemoryRouter } from 'react-router-dom'; import { formEventUpdate, saveEventUpdate } from 'form-builder/actions/control'; +import { formBuilderConstants } from 'form-builder/constants'; chai.use(chaiEnzyme()); @@ -102,7 +103,8 @@ describe('Import form', () => { let wrapper; let breadcrumbsStub; const saveFormSpy = sinon.spy(); - const saveFormResourceSpy = sinon.spy(); + const saveFormResourceSpy = sinon.spy(() => + Promise.resolve({ id: 201, uuid: 'new_uuid', version: 1 })); const dispatchSpy = sinon.spy(); const file = [ { @@ -428,6 +430,140 @@ describe('Import form', () => { done(); }, 500); }); + + it('should save imported form privileges against the newly created form', (done) => { + const formBuilderInstance = wrapper.find('FormBuilder').instance(); + const privileges = [ + { privilegeName: 'sample', editable: true, viewable: false }, + ]; + const formJson = { + form: { name: 'Vitals', version: '1', published: false }, + value: { name: 'Vitals', controls: [] }, + formName: 'Vitals', + translations: [], + nameTranslations: undefined, + privileges, + }; + sinon.stub(httpInterceptor, 'post').callsFake((url) => { + if (url === formBuilderConstants.saveFormPrivilegesUrl) { + return Promise.resolve(); + } + return Promise.resolve(Object.assign({}, data[0], { uuid: 'new_uuid' })); + }); + formBuilderInstance.importValidForms([formJson]); + setTimeout(() => { + const privilegesCall = httpInterceptor.post.getCalls() + .find((call) => call.args[0] === formBuilderConstants.saveFormPrivilegesUrl); + expect(privilegesCall).not.to.eql(undefined); + expect(privilegesCall.args[1]).to.deep.eql([ + { + formId: data[0].id, + formVersion: data[0].version, + privilegeName: 'sample', + editable: true, + viewable: false, + }, + ]); + done(); + }, 500); + }); + + it('should not call saveFormPrivileges when the imported form has no privileges', (done) => { + const formBuilderInstance = wrapper.find('FormBuilder').instance(); + const formJson = { + form: { name: 'Vitals', version: '1', published: false }, + value: { name: 'Vitals', controls: [] }, + formName: 'Vitals', + translations: [], + nameTranslations: undefined, + privileges: [], + }; + sinon.stub(httpInterceptor, 'post').callsFake(() => + Promise.resolve(Object.assign({}, data[0], { uuid: 'new_uuid' }))); + formBuilderInstance.importValidForms([formJson]); + setTimeout(() => { + const privilegesCall = httpInterceptor.post.getCalls() + .find((call) => call.args[0] === formBuilderConstants.saveFormPrivilegesUrl); + expect(privilegesCall).to.eql(undefined); + done(); + }, 500); + }); + + it('should save privileges and wait for completion on the "form already exists" ' + + 'fallback path', (done) => { + const callOrder = []; + const onImportCompleteSpy = sinon.spy(() => callOrder.push('onImportComplete')); + const fallbackSaveFormResourceSpy = sinon.spy(() => + Promise.resolve({ id: 301, uuid: 'existing_form_uuid', version: 2 })); + const localWrapper = mount(); + const formBuilderInstance = localWrapper.find('FormBuilder').instance(); + const privileges = [ + { privilegeName: 'sample', editable: true, viewable: false }, + ]; + const formJson = { + form: { name: '1', version: '1', published: false }, + value: { name: '1', controls: [] }, + formName: '1', + translations: [], + nameTranslations: undefined, + privileges, + }; + sinon.stub(httpInterceptor, 'post').callsFake((url) => { + if (url === formBuilderConstants.saveFormPrivilegesUrl) { + callOrder.push('privileges'); + return Promise.resolve(); + } + return Promise.reject(new Error('form already exists')); + }); + sinon.stub(httpInterceptor, 'get').callsFake(() => + Promise.resolve({ resources: [{ uuid: 'resource_uuid' }] })); + formBuilderInstance.importValidForms([formJson]); + setTimeout(() => { + sinon.assert.calledOnce(fallbackSaveFormResourceSpy); + const privilegesCall = httpInterceptor.post.getCalls() + .find((call) => call.args[0] === formBuilderConstants.saveFormPrivilegesUrl); + expect(privilegesCall).not.to.eql(undefined); + expect(privilegesCall.args[1]).to.deep.eql([ + { + formId: 301, + formVersion: 2, + privilegeName: 'sample', + editable: true, + viewable: false, + }, + ]); + sinon.assert.calledOnce(onImportCompleteSpy); + expect(callOrder).to.eql(['privileges', 'onImportComplete']); + done(); + }, 500); + }); + + it('should call onImportComplete once after all forms in the batch are imported', (done) => { + const onImportCompleteSpy = sinon.spy(); + const localWrapper = mount(); + const formBuilderInstance = localWrapper.find('FormBuilder').instance(); + const formJsons = ['Vitals1', 'Vitals2'].map((name) => ({ + form: { name, version: '1', published: false }, + value: { name, controls: [] }, + formName: name, + translations: [], + nameTranslations: undefined, + privileges: [], + })); + sinon.stub(httpInterceptor, 'post').callsFake(() => + Promise.resolve(Object.assign({}, data[0], { uuid: 'new_uuid' }))); + formBuilderInstance.importValidForms(formJsons); + setTimeout(() => { + sinon.assert.calledOnce(onImportCompleteSpy); + done(); + }, 500); + }); }); describe('Export Forms', () => { @@ -516,6 +652,9 @@ describe('Export Forms', () => { }; mockHttp.get.withArgs('/openmrs/ws/rest/v1/bahmniie/form/export?uuid=uuid1') .returns(Promise.resolve(exportResponse)); + mockHttp.get + .withArgs('/openmrs/ws/rest/v1/bahmniie/form/getFormPrivilegesFromUuid?formUuid=undefined') + .returns(Promise.resolve([])); wrapper.instance().exportForms(); setTimeout(() => { sinon.assert.calledTwice(spyZipFile); @@ -523,6 +662,32 @@ describe('Export Forms', () => { done(); }, 50); }); + + it('should include privileges when exporting multiple forms', (done) => { + if (JSZip.prototype.file.restore !== undefined) { + JSZip.prototype.file.restore(); + } + const spyZipFile = sinon.spy(JSZip.prototype, 'file'); + wrapper.instance().state.selectedForms = ['uuid1']; + const privileges = [{ formId: 1, privilegeName: 'sample', editable: true, viewable: false }]; + exportResponse = { + bahmniFormDataList: [{ formJson: { name: 'Form', version: '1', uuid: 'uuid1' } }], + errorFormList: [], + }; + mockHttp.get.withArgs('/openmrs/ws/rest/v1/bahmniie/form/export?uuid=uuid1') + .returns(Promise.resolve(exportResponse)); + mockHttp.get + .withArgs('/openmrs/ws/rest/v1/bahmniie/form/getFormPrivilegesFromUuid?formUuid=uuid1') + .returns(Promise.resolve(privileges)); + wrapper.instance().exportForms(); + setTimeout(() => { + sinon.assert.calledOnce(spyZipFile); + const zippedContent = JSON.parse(spyZipFile.getCall(0).args[1]); + expect(zippedContent.formJson.privileges).to.deep.eql(privileges); + spyZipFile.restore(); + done(); + }, 50); + }); }); describe('Import Multiple Forms', () => {