...
In this example, Relationship and RelationshipType already exists in the OpenMRS data model, and there are resources for them in the webservices.rest module: RelationshipResource1_8, RelationShipTypeResource1RelationshipTypeResource1_8.
If you are developing a new kind of data, you will need to create a Java and REST API (which is not covered here).
...
Note that this is a new pattern for OpenMRS, so we welcome feedback about how to improve it!
The latest version of this code (with less commentary) lives at relationshipService.js.
Step 2 - Create a new screen to manage your data
...
Code Block | ||||
---|---|---|---|---|
| ||||
package org.openmrs.module.coreapps.page.controller.relationships; import org.openmrs.module.coreapps.helper.SimplePatientPageController; public class ListPageController extends SimplePatientPageController { // GET defined in superclass } |
...
Code Block | ||||
---|---|---|---|---|
| ||||
<% // Nearly every page you write should do this, to get the standard reference application decoration and includes ui.decorateWith("appui", "standardEmrPage") // include angular and angular-uiplugins ui.includeJavascript("uicommons", "angular.js") ui.includeJavascript("uicommons", "angular-resource.min.js") ui.includeJavascript("uicommons", "angular-ui/ui-bootstrap-tpls-0.6.0.min.js") // we use the following OpenMRS angular services and directives ui.includeJavascript("uicommons", "angular-commonngDialog/ngDialog.js") ui.includeJavascriptincludeCss("uicommons", "servicesngDialog/relationshipService.ngDialog.min.css") // we use the following OpenMRS angular services and directives ui.includeJavascript("uicommons", "angular-common.js") ui.includeJavascript("uicommons", "services/relationshipService.js") ui.includeJavascript("uicommons", "services/relationshipTypeService.js") ui.includeJavascript("uicommons", "services/personService.js") ui.includeJavascript("uicommons", "directives/select-person.js") // custom JS and CSS for this page ui.includeJavascript("coreapps", "relationships/relationships.js") ui.includeCss("coreapps", "relationships/relationshipslist.css") %> <!-- Configuring breadcrumbs; you'd typically copy-paste this from another page and change the last item in the list --> <script type="text/javascript"> var breadcrumbs = [ { icon: "icon-home", link: '/' + OPENMRS_CONTEXT_PATH + '/index.htm' }, { label: "${ ui.escapeJs(patient.patient.familyName) }, ${ ui.escapeJs(patient.patient.givenName) }" , link: '${ui.pageLink("coreapps", "clinicianfacing/patient", [patientId: patient.patient.id])}'}, { label: "${ ui.escapeJs(ui.message("coreapps.task.relationships.label")) }" } ] </script> <!-- Include the standard patient header before we get to the specifics of this page --> ${ ui.includeFragment("coreapps", "patientHeader", [ patient: patient.patient ]) } <!-- Your page should have a title --> <h2>$<h3>${ ui.message("coreapps.task.relationships.label") }</h2>h3> <div id="relationships-app <!-- ng-init is the most convenient way to bootstrap the static js code with dynamic data from this gsp --> <div id="relationships-app" ng-controller="PersonRelationshipsCtrl" ng-init="init('${ patient.patient.uuid }')"> <div ng-show="addDialogMode" class="dialog<!-- This is how you do an inline angular template --> <script type="text/ng-template" id="add-relationship-dialogaddDialogTemplate"> <div class="dialog-header"> <%<h3><%= ui.message("coreapps.relationships.add.header", "{{ addDialogOtherLabelngDialogData.otherLabel }}") %>%></h3> </div> <div class="dialog-content"> <div> <label> <%= ui.message("coreapps.relationships.add.choose", "{{ addDialogOtherLabelngDialogData.otherLabel }}") %> </label> <select-person ng-model="otherPerson" exclude-person="${ patient.patient.uuid }" /> </div> <div class="add-confirm-spacer"> <div ng-show="otherPerson" > <h3>${ ui.message("coreapps.relationships.add.confirm") }</h3> <p>{{ addDialogThisLabelngDialogData.thisLabel }}: ${ ui.format(patient.patient) } ${ ui.message("coreapps.relationships.add.thisPatient") }</p> <p>{{ addDialogOtherLabelngDialogData.otherLabel }}: {{ otherPerson.display }}</p> </div> </div> <div> <button class="confirm right" ng-disabled="!otherPerson" ng-click="doAddRelationshipconfirm(otherPerson)">${ ui.message("uicommons.save") }</button> <button class="cancel" ng-click="cancelAddRelationshipcloseThisDialog()">${ ui.message("uicommons.cancel") }</button> </div> </div> </div>script> <div<script type="text/ng-show=template"relationshipToDelete" class="dialog" id="delete-relationship-dialog"> <div class="dialog-header">${ ui.message("coreapps.relationships.delete.header") }</div>"deleteDialogTemplate"> <div class="dialog-contentheader"> <form> $<h3>${ ui.message("coreapps.relationships.delete.titleheader") }</h3> </div> <p> <label>{{ relType(relationshipToDelete).aIsToB }}</label> <div class="dialog-content"> <form> {{ relationshipToDelete.personA.display }} ${ ui.message("coreapps.relationships.delete.title") } </p> <p> <label>{{ relType(relationshipToDeletengDialogData.relationship).bIsToAaIsToB }}</label> {{ relationshipToDeletengDialogData.relationship.personBpersonA.display }} </p> <button class="confirm right" ng-click="doDeleteRelationship(relationshipToDelete)">${ ui.message("uicommons.delete") }</button> </p> <p> <button class="cancel" ng-click="cancelDeleteRelationship(relationshipToDelete)">${ ui.message("uicommons.cancel") }</button> <label>{{ relType(ngDialogData.relationship).bIsToA }}</form>label> </div> </div> <form id="existing-relationships"> {{ ngDialogData.relationship.personB.display }} <div ng-repeat="relType in relationshipTypes"> </p> <p> <button class="confirm <label>{{ relType.aIsToB }}</label>right" ng-click="confirm()">${ ui.message("uicommons.delete") }</button> <button <spanclass="cancel" ng-repeat="rel in relationshipsByType(relType, 'A')" class="relationship">click="closeThisDialog()">${ ui.message("uicommons.cancel") }</button> </form> </div> {{ rel.personA.display }} </script> <div ng-repeat="relType in relationshipTypes"> <h6> <a ng-click="showDeleteDialog(rel)"> {{ relType.aIsToB }} <i<a classng-click="small icon-remove"></i> showAddDialog(relType, 'A')"> <i </a>class="icon-plus-sign edit-action"></i> </span>a> </h6> <span ng-show="relType.aIsToB == relType.bIsToA" ng-repeat="rel in relationshipsByType(relType, 'BA')" class="relationship"> {{ rel.personBpersonA.display }} <a ng-click="showDeleteDialog(rel)"> <i class="icon-remove delete-action"></i> <i class="small icon-remove"></i> </a> </span> <span ng-show="relType.aIsToB </a> == relType.bIsToA" ng-repeat="rel in relationshipsByType(relType, 'B')" class="relationship"> </span> {{ rel.personB.display }} <a ng-click="showAddDialog(relType, 'A'showDeleteDialog(rel)"> <i class="small icon-plusremove delete-signaction"></i> i> </a> </p>span> <span <p ng-hide="relType.aIsToB == relType.bIsToA"> <h6> <label> {{ relType.bIsToA }}</label> <span<a ng-repeatclick="rel in relationshipsByTypeshowAddDialog(relType, 'B')" class="relationship"> {{ rel.personB.display }} <i class="icon-plus-sign edit-action"></i> <a ng-click="showDeleteDialog(rel)"> </a> <i class="small icon-remove"></i></h6> <span ng-repeat="rel in relationshipsByType(relType, 'B')" class="relationship"> </a> {{ </span>rel.personB.display }} <a ng-click="showAddDialog(relType, 'B'showDeleteDialog(rel)"> <i class="small icon-plusremove delete-signaction"></i> </a> </p>span> </div>span> </form>div> </div> <script type="text/javascript"> // manually bootstrap angular app, in case there are multiple angular apps on a page angular.bootstrap('#relationships-app', ['relationships']); </script> |
...
Code Block | ||||
---|---|---|---|---|
| ||||
// a new "module" just for this page that depends on various modules from uicommons angular.module('relationships', ['relationshipTypeService', 'relationshipService', 'personService', 'uicommons.widget.select-person', 'ngDialog' ]). // this page has a single controller, which gets several dependencies injected controller('PersonRelationshipsCtrl', ['$scope', 'RelationshipTypeService', 'RelationshipService', 'PersonService', '$modal', 'ngDialog', function($scope, RelationshipTypeService, RelationshipService, PersonService, $modal, ngDialog) { // the model for our view $scope.thisPersonUuid = null; $scope.relationshipTypes = []; $scope.relationships = []; // this is invoked by ng-init="init($patient.patient.uuid)" in the gsp page, which is how we communicate that specific bit of // bootstrapping data to our javascript app $scope.init = function(personUuid) { $scope.thisPersonUuid = personUuid; // these calls (which use $resource under the hood) are asynchronous, so we need to use .then on the promise they return RelationshipService.getRelationships({ v: 'default', person: personUuid }).then(function(result) { $scope.relationships = result; }); RelationshipTypeService.getRelationshipTypes({ v: 'default' }).then(function(result) { $scope.relationshipTypes = result; }); } // Helper method, since after saving a relationship, the response object we get back from the OpenMRS REST resource does not // include a full rep of its relationshipType, so this helper gets the full rep that we download on init $scope.relType = function(relationship) { if (!relationship) { return null; } return _.findWhere($scope.relationshipTypes, { uuid: relationship.relationshipType.uuid }); } $scope.relationshipsByType = function(relationshipType, whichSide) { return _.filter($scope.relationships, function(item) { if (item.relationshipType.uuid != relationshipType.uuid) { return false; } if (whichSide == 'A' && item.personB.uuid == $scope.thisPersonUuid) { return true; } else if (whichSide == 'B' && item.personA.uuid == $scope.thisPersonUuid) { return true; } (whichSide == 'B' && item.personA.uuid == $scope.thisPersonUuid) { }); } return true; // ---------- // Add New Relationship Dialog} (maybe this should be refactored to be its own controller and child scope}); // ----------} $scope.addDialogMode = null; showAddDialog = function(relationshipType, whichSide) { $scope.addDialogThisLabel = ''; $scope.addDialogOtherLabel = '';ngDialog.openConfirm({ $scope.otherPerson = null; showClose: false, $scope.showAddDialog = function(relationshipType, whichSide) { closeByEscape: true, $scope.otherPerson = null; closeByDocument: true, $scope.addDialogMode = { data: angular.toJson({ relationshipType: relationshipType, whichSideotherLabel: whichSide == 'A' ? relationshipType.aIsToB : relationshipType.bIsToA, }; $scope.addDialogOtherLabel =thisLabel: whichSide == 'A' ? relationshipType.aIsToBbIsToA : relationshipType.bIsToA;aIsToB $scope.addDialogThisLabel = whichSide == 'A' ? relationshipType.bIsToA : relationshipType.aIsToB;}), template: $('#select-other-person').focus();'addDialogTemplate' }). $scope.doAddRelationship = then(function(otherPerson) { var relationship = { relationshipType: $scope.addDialogMode.relationshipType.uuid, personA: $scope.addDialogMode. whichSide == 'A' ? $scope.otherPerson.uuid : $scope.thisPersonUuid, personB: $scope.addDialogMode.whichSide == 'A' ? $scope.thisPersonUuid : $scope.otherPerson.uuid }; var created = RelationshipService.createRelationship(relationship); $scope.relationships.push(created); $scope.otherPerson = null}); $scope.addDialogMode = nullangular.element('#select-other-person').focus(); } $scope.cancelAddRelationshipshowDeleteDialog = function(relationship) { $scope.otherPerson = null; $scope.addDialogMode = null; ngDialog.openConfirm({ } // ---------- showClose: false, // Delete Relationship Dialog (maybe this should be refactored tocloseByEscape: betrue, its own controller and child scope) // ---------- closeByDocument: true, $scope.relationshipToDelete = null; template: 'deleteDialogTemplate', $scope.showDeleteDialog = function(relationship) { data: $scopeangular.relationshipToDelete =toJson({ relationship: relationship; }), } $scope.doDeleteRelationship = functionscope: $scope // need this so the view can call the $scope.relType(relationship) {function RelationshipService.deleteRelationship(relationship);}). $scope.relationships = _.reject($scope.relationships, then(function(itemrelationshipToDelete) { RelationshipService.deleteRelationship(relationship); return item.uuid == relationship.uuid; $scope.relationships = });_.reject($scope.relationships, function(item) { $scope.relationshipToDelete = null; return item.uuid == relationship.uuid; } $scope.cancelDeleteRelationship = function(relationship) { }); $scope.relationshipToDelete = null}); } }]); |
The current, latest version of this code (with fewer comments) can be found at relationships/list.gsp and relationships.js.
Step 3 - Use an Extension to link to your screen from the patient summary
...
We add the following to the overallActions_extension.json file in the coreapps module. See App Framework Developer Documentation if you want detailed documentation on this structure.
...