forms-angular 0.12.0-beta.21 → 0.12.0-beta.210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,22 +4,59 @@ var fng;
4
4
  var directives;
5
5
  (function (directives) {
6
6
  /*@ngInject*/
7
- function modelControllerDropdown() {
7
+ modelControllerDropdown.$inject = ["securityService"];
8
+ function modelControllerDropdown(securityService) {
9
+ var menuVisibilityStr;
10
+ var menuDisabledStr;
11
+ var itemVisibilityStr = "isHidden($index)";
12
+ var itemDisabledStr = "ng-class=\"dropdownClass($index)\"";
13
+ // without a more fundamental re-write, we cannot support "instant" binding here, so we'll fall-back to using
14
+ // the next-best alternative, which is one-time binding
15
+ var oneTimeBinding = fng.formsAngular.elemSecurityFuncBinding !== "normal";
16
+ var bindingStr = oneTimeBinding ? "::" : "";
17
+ if (securityService.canDoSecurity("hidden")) {
18
+ menuVisibilityStr = "ng-if=\"contextMenuId && !contextMenuHidden\" ".concat(securityService.getHideableAttrs('{{ ::contextMenuId }}'));
19
+ if (oneTimeBinding) {
20
+ // because the isHidden(...) logic is highly likely to be model dependent, that cannot be one-time bound. to
21
+ // be able to combine one-time and regular binding, we'll use ng-if for the one-time bound stuff and ng-hide for the rest
22
+ itemVisibilityStr = "ng-if=\"::(choice.divider || !isSecurelyHidden(choice.id))\" ng-hide=\"".concat(itemVisibilityStr, "\"");
23
+ }
24
+ else if (fng.formsAngular.elemSecurityFuncBinding === "normal") {
25
+ itemVisibilityStr = "ng-hide=\"".concat(itemVisibilityStr, " || (!choice.divider && isSecurelyHidden(choice.id))\"");
26
+ }
27
+ itemVisibilityStr += " ".concat(securityService.getHideableAttrs('{{ ::choice.id }}'));
28
+ }
29
+ else {
30
+ menuVisibilityStr = "";
31
+ itemVisibilityStr = "ng-hide=\"".concat(itemVisibilityStr, "\"");
32
+ }
33
+ if (securityService.canDoSecurity("disabled")) {
34
+ menuDisabledStr = "disableable-link ng-disabled=\"contextMenuDisabled\" ".concat(securityService.getDisableableAttrs('{{ ::contextMenuId }}'));
35
+ // as ng-class is already being used, we'll add the .disabled class if the menu item is securely disabled using
36
+ // class="{{ }}". note that we "prevent" a disabled menu item from being clicked by checking for the DISABLED
37
+ // attribute in the doClick(...) function, and aborting if this is found.
38
+ // note that the 'normal' class introduced here might not actually do anything, but for one-time binding to work
39
+ // properly, we need a truthy value
40
+ itemDisabledStr += " class=\"{{ ".concat(bindingStr, "(!choice.divider && isSecurelyDisabled(choice.id)) ? 'disabled' : 'normal' }}\" ").concat(securityService.getDisableableAttrs('{{ ::choice.id }}'));
41
+ }
42
+ else {
43
+ menuDisabledStr = "";
44
+ }
8
45
  return {
9
- restrict: 'AE',
46
+ restrict: "AE",
10
47
  replace: true,
11
- template: '<li ng-show="items.length > 0" class="mcdd" uib-dropdown>' +
12
- ' <a uib-dropdown-toggle>' +
48
+ template: "<li id=\"{{ contextMenuId }}\" ng-show=\"items.length > 0\" class=\"mcdd\" uib-dropdown ".concat(menuVisibilityStr, " ").concat(menuDisabledStr, ">") +
49
+ ' <a href="#" uib-dropdown-toggle>' +
13
50
  ' {{contextMenu}} <b class="caret"></b>' +
14
- ' </a>' +
51
+ " </a>" +
15
52
  ' <ul class="uib-dropdown-menu dropdown-menu">' +
16
- ' <li ng-repeat="choice in items" ng-hide="isHidden($index)" ng-class="dropdownClass($index)">' +
17
- ' <a ng-show="choice.text" class="dropdown-option" ng-href="{{choice.url}}" ng-click="doClick($index, $event)">' +
18
- ' {{choice.text}}' +
19
- ' </a>' +
20
- ' </li>' +
21
- ' </ul>' +
22
- '</li>'
53
+ " <li ng-repeat=\"choice in items\" ng-attr-id=\"{{choice.id}}\" ".concat(itemVisibilityStr, " ").concat(itemDisabledStr, ">") +
54
+ ' <a ng-show="choice.text || choice.textFunc" class="dropdown-option" ng-href="{{choice.url || choice.urlFunc()}}" ng-click="doClick($index, $event)">' +
55
+ " {{ choice.text || choice.textFunc() }}" +
56
+ " </a>" +
57
+ " </li>" +
58
+ " </ul>" +
59
+ "</li>",
23
60
  };
24
61
  }
25
62
  directives.modelControllerDropdown = modelControllerDropdown;
@@ -31,16 +68,11 @@ var fng;
31
68
  var directives;
32
69
  (function (directives) {
33
70
  /*@ngInject*/
34
- function errorDisplay() {
71
+ errorDisplay.$inject = ["cssFrameworkService"];
72
+ function errorDisplay(cssFrameworkService) {
35
73
  return {
36
74
  restrict: 'E',
37
- template: '<div id="display-error" ng-show="errorMessage" ng-class="css(\'rowFluid\')">' +
38
- ' <div class="alert alert-error col-lg-offset-3 offset3 col-lg-6 col-xs-12 span6 alert-warning alert-dismissable">' +
39
- ' <button type="button" class="close" ng-click="dismissError()">×</button>' +
40
- ' <h4>{{alertTitle}}</h4>' +
41
- ' <div ng-bind-html="errorMessage"></div>' +
42
- ' </div>' +
43
- '</div>'
75
+ templateUrl: 'error-display-' + cssFrameworkService.framework() + '.html'
44
76
  };
45
77
  }
46
78
  directives.errorDisplay = errorDisplay;
@@ -48,6 +80,44 @@ var fng;
48
80
  })(fng || (fng = {}));
49
81
  /// <reference path="../../../../node_modules/@types/angular/index.d.ts" />
50
82
  var fng;
83
+ (function (fng) {
84
+ var controllers;
85
+ (function (controllers) {
86
+ /*@ngInject*/
87
+ LinkCtrl.$inject = ["$scope"];
88
+ function LinkCtrl($scope) {
89
+ /**
90
+ * In the event that the link is part of a form that belongs to a (ui-bootstrap) modal,
91
+ * close the modal
92
+ */
93
+ $scope.checkNotModal = function () {
94
+ var elm = $scope.element[0];
95
+ var parentNode;
96
+ var finished = false;
97
+ var fakeEvt = {
98
+ preventDefault: angular.noop,
99
+ stopPropagation: angular.noop,
100
+ target: 1,
101
+ currentTarget: 1
102
+ };
103
+ do {
104
+ parentNode = elm.parentNode;
105
+ if (!parentNode) {
106
+ finished = true;
107
+ }
108
+ else if (typeof parentNode.getAttribute === "function" && parentNode.getAttribute('uib-modal-window')) {
109
+ angular.element(elm).scope().close(fakeEvt);
110
+ finished = true;
111
+ }
112
+ else {
113
+ elm = parentNode;
114
+ }
115
+ } while (!finished);
116
+ };
117
+ }
118
+ controllers.LinkCtrl = LinkCtrl;
119
+ })(controllers = fng.controllers || (fng.controllers = {}));
120
+ })(fng || (fng = {}));
51
121
  (function (fng) {
52
122
  var directives;
53
123
  (function (directives) {
@@ -58,38 +128,124 @@ var fng;
58
128
  restrict: 'E',
59
129
  scope: { dataSrc: '&model' },
60
130
  link: function (scope, element, attrs) {
61
- var ref = JSON.parse(attrs['ref']);
131
+ var ref = attrs['ref'];
132
+ var isLabel = attrs['text'] && (unescape(attrs['text']) !== attrs['text']);
62
133
  var form = attrs['form'];
134
+ var linktab = attrs['linktab'];
63
135
  scope['readonly'] = attrs['readonly'];
136
+ scope['element'] = element;
64
137
  form = form ? form + '/' : '';
65
- if (attrs['text'] && attrs['text'].length > 0) {
66
- scope['text'] = attrs['text'];
67
- }
68
- var index = scope['$parent']['$index'];
69
- scope.$watch('dataSrc()', function (newVal) {
70
- if (newVal) {
71
- if (typeof index !== 'undefined' && angular.isArray(newVal)) {
72
- newVal = newVal[index];
73
- }
74
- scope['link'] = routingService.buildUrl(ref.collection + '/' + form + newVal + '/edit');
75
- if (!scope['text']) {
76
- SubmissionsService.getListAttributes(ref, newVal).then(function (response) {
77
- var data = response.data;
78
- if (data.success === false) {
79
- scope['text'] = data.err;
138
+ linktab = linktab ? '/' + linktab : '';
139
+ if (isLabel) {
140
+ var workScope = scope;
141
+ var workString = '';
142
+ while (typeof workScope['baseSchema'] !== "function" && workScope.$parent) {
143
+ if (typeof workScope['$index'] !== "undefined") {
144
+ throw new Error('No support for $index at this level - ' + workString);
145
+ }
146
+ workScope = workScope.$parent;
147
+ workString = workString + '$parent.';
148
+ }
149
+ var attrib = attrs['fld'];
150
+ var watchExpression;
151
+ var splitAttrib = attrib.split('.');
152
+ if (scope.$parent.subDoc && (scope.$parent.subDoc[attrib] || scope.$parent.subDoc[splitAttrib[splitAttrib.length - 1]])) {
153
+ // Support for use in directives in arrays
154
+ if (scope.$parent.subDoc[attrib]) {
155
+ watchExpression = workString + 'subDoc.' + attrib;
156
+ }
157
+ else {
158
+ watchExpression = workString + 'subDoc.' + splitAttrib[splitAttrib.length - 1];
159
+ }
160
+ }
161
+ else {
162
+ if (typeof workScope['$index'] !== "undefined") {
163
+ attrib = splitAttrib.pop();
164
+ attrib = splitAttrib.join('.') + '[' + workScope['$index'] + '].' + attrib;
165
+ }
166
+ else {
167
+ attrib = '.' + attrib;
168
+ }
169
+ watchExpression = workString + 'record' + attrib;
170
+ }
171
+ scope.$watch(watchExpression, function (newVal) {
172
+ if (newVal) {
173
+ if (/^[a-f0-9]{24}/.test(newVal.toString())) {
174
+ newVal = newVal.slice(0, 24);
175
+ }
176
+ else if (newVal.id && /^[a-f0-9]{24}/.test(newVal.id)) {
177
+ newVal = newVal.id.slice(0, 24);
178
+ }
179
+ else if (scope.$parent["f_" + attrs['fld'] + "Options"]) {
180
+ // extract from lookups
181
+ var i = scope.$parent["f_" + attrs['fld'] + "Options"].indexOf(newVal);
182
+ if (i > -1) {
183
+ newVal = scope.$parent["f_" + attrs['fld'] + "_ids"][i];
80
184
  }
81
185
  else {
82
- scope['text'] = data.list;
186
+ newVal = undefined;
83
187
  }
84
- }, function (response) {
85
- scope['text'] = 'Error ' + response.status + ': ' + response.data;
86
- });
188
+ }
189
+ else {
190
+ newVal = undefined;
191
+ }
192
+ if (newVal) {
193
+ scope['link'] = routingService.buildUrl(ref + '/' + form + (newVal.id || newVal) + '/edit' + linktab);
194
+ }
195
+ else {
196
+ scope['link'] = undefined;
197
+ }
198
+ }
199
+ else {
200
+ scope['link'] = undefined;
87
201
  }
202
+ }, true);
203
+ }
204
+ else {
205
+ if (attrs['text'] && attrs['text'].length > 0) {
206
+ scope['text'] = attrs['text'];
88
207
  }
89
- }, true);
208
+ var index = scope['$parent']['$index'];
209
+ scope.$watch('dataSrc()', function (newVal) {
210
+ if (newVal) {
211
+ if (typeof index !== 'undefined' && angular.isArray(newVal)) {
212
+ newVal = newVal[index];
213
+ }
214
+ scope['link'] = routingService.buildUrl(ref + '/' + form + newVal + '/edit' + linktab);
215
+ if (!scope['text']) {
216
+ SubmissionsService.getListAttributes(ref, newVal).then(function (response) {
217
+ var data = response.data;
218
+ if (data.success === false) {
219
+ scope['text'] = data.err;
220
+ }
221
+ else {
222
+ scope['text'] = data.list;
223
+ }
224
+ }, function (response) {
225
+ scope['text'] = 'Error ' + response.status + ': ' + response.data;
226
+ });
227
+ }
228
+ }
229
+ }, true);
230
+ }
90
231
  },
232
+ controller: "LinkCtrl",
91
233
  template: function (element, attrs) {
92
- return attrs.readonly ? '<span class="fng-link">{{text}}</span>' : '<a href="{{ link }}" class="fng-link">{{text}}</a>';
234
+ function handleAnchor(contents) {
235
+ return "<a ng-click=\"checkNotModal()\" ng-href=\"{{ link || '#' }}\" class=\"fng-link\">".concat(contents, "</a>");
236
+ }
237
+ var retVal;
238
+ if (attrs.readonly) {
239
+ retVal = '<span class="fng-link">{{text}}</span>';
240
+ }
241
+ else if (attrs['text'] && unescape(attrs['text']) !== attrs['text']) {
242
+ retVal = handleAnchor(unescape(attrs['text']));
243
+ // retVal = '<a href="{{ link }}" class="fng-link">{{text}}</a>';
244
+ }
245
+ else {
246
+ retVal = handleAnchor('{{text}}');
247
+ }
248
+ return retVal;
93
249
  }
94
250
  };
95
251
  }
@@ -117,7 +273,7 @@ var fng;
117
273
  (function (fng) {
118
274
  var directives;
119
275
  (function (directives) {
120
- formInput.$inject = ["$compile", "$rootScope", "$filter", "$timeout", "cssFrameworkService", "formGenerator", "formMarkupHelper"];
276
+ formInput.$inject = ["$compile", "$rootScope", "$filter", "$timeout", "cssFrameworkService", "formGenerator", "formMarkupHelper", "securityService"];
121
277
  var tabsSetupState;
122
278
  (function (tabsSetupState) {
123
279
  tabsSetupState[tabsSetupState["Y"] = 0] = "Y";
@@ -125,7 +281,7 @@ var fng;
125
281
  tabsSetupState[tabsSetupState["Forced"] = 2] = "Forced";
126
282
  })(tabsSetupState || (tabsSetupState = {}));
127
283
  /*@ngInject*/
128
- function formInput($compile, $rootScope, $filter, $timeout, cssFrameworkService, formGenerator, formMarkupHelper) {
284
+ function formInput($compile, $rootScope, $filter, $timeout, cssFrameworkService, formGenerator, formMarkupHelper, securityService) {
129
285
  return {
130
286
  restrict: 'EA',
131
287
  link: function (scope, element, attrs) {
@@ -146,7 +302,7 @@ var fng;
146
302
  // <input type="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
147
303
  // </div>
148
304
  //
149
- // Inline
305
+ // Inline or stacked
150
306
  // <div class="form-group">
151
307
  // <label class="sr-only" for="exampleInputEmail2">Email address</label>
152
308
  // <input type="email" class="form-control" id="exampleInputEmail2" placeholder="Enter email">
@@ -165,11 +321,11 @@ var fng;
165
321
  // <input type="text" placeholder="Type something…">
166
322
  // <span class="help-block">Example block-level help text here.</span>
167
323
  //
168
- // Inline
324
+ // Inline or Stacked
169
325
  // <input type="text" class="input-small" placeholder="Email">
170
326
  var subkeys = [];
171
327
  var tabsSetup = tabsSetupState.N;
172
- var generateInput = function (fieldInfo, modelString, isRequired, idString, options) {
328
+ var generateInput = function (fieldInfo, modelString, isRequired, options) {
173
329
  function generateEnumInstructions() {
174
330
  var enumInstruction;
175
331
  if (angular.isArray(scope[fieldInfo.options])) {
@@ -195,6 +351,10 @@ var fng;
195
351
  }
196
352
  return enumInstruction;
197
353
  }
354
+ var idString = fieldInfo.id;
355
+ if (fieldInfo.array || options.subschema) {
356
+ idString = formMarkupHelper.generateArrayElementIdString(idString, fieldInfo, options);
357
+ }
198
358
  var nameString;
199
359
  if (!modelString) {
200
360
  var modelBase = (options.model || 'record') + '.';
@@ -206,7 +366,7 @@ var fng;
206
366
  var lastPart = compoundName.slice(root.length + 1);
207
367
  if (options.index) {
208
368
  modelString += root + '[' + options.index + '].' + lastPart;
209
- idString = 'f_' + modelString.slice(modelBase.length).replace(/(\.|\[|\]\.)/g, '-');
369
+ idString = 'f_' + modelString.slice(modelBase.length).replace(/(\.|\[|]\.)/g, '-');
210
370
  }
211
371
  else {
212
372
  modelString += root;
@@ -216,7 +376,6 @@ var fng;
216
376
  }
217
377
  else {
218
378
  modelString += '[$index].' + lastPart;
219
- idString = null;
220
379
  nameString = compoundName.replace(/\./g, '-');
221
380
  }
222
381
  }
@@ -228,9 +387,8 @@ var fng;
228
387
  var allInputsVars = formMarkupHelper.allInputsVars(scope, fieldInfo, options, modelString, idString, nameString);
229
388
  var common = allInputsVars.common;
230
389
  var value;
231
- var requiredStr = (isRequired || fieldInfo.required) ? ' required' : '';
232
390
  isRequired = isRequired || fieldInfo.required;
233
- var requiredStr = isRequired ? ' required' : '';
391
+ var requiredStr = isRequired ? ' required ' : '';
234
392
  var enumInstruction;
235
393
  switch (fieldInfo.type) {
236
394
  case 'select':
@@ -238,8 +396,9 @@ var fng;
238
396
  value = '<input placeholder="fng-select2 has been removed" readonly>';
239
397
  }
240
398
  else {
241
- common += (fieldInfo.readonly ? 'disabled ' : '');
399
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
242
400
  common += fieldInfo.add ? (' ' + fieldInfo.add + ' ') : '';
401
+ common += " aria-label=\"".concat(fieldInfo.label && fieldInfo.label !== "" ? fieldInfo.label : fieldInfo.name, "\" ");
243
402
  value = '<select ' + common + 'class="' + allInputsVars.formControl.trim() + allInputsVars.compactClass + allInputsVars.sizeClassBS2 + '" ' + requiredStr + '>';
244
403
  if (!isRequired) {
245
404
  value += '<option></option>';
@@ -268,25 +427,32 @@ var fng;
268
427
  }
269
428
  break;
270
429
  case 'link':
271
- if (typeof fieldInfo.ref === 'string') {
272
- fieldInfo.ref = { type: 'lookup', collection: fieldInfo.ref };
273
- console.warn("Support for string type \"ref\" property is deprecated - use ref:" + JSON.stringify(fieldInfo.ref));
274
- }
275
- value = '<fng-link model="' + modelString + '" ref="' + JSON.stringify(fieldInfo.ref).replace(/"/g, '&quot;') + '"';
430
+ value = '<fng-link model="' + modelString + '" ref="' + fieldInfo.ref + '"';
276
431
  if (fieldInfo.form) {
277
432
  value += ' form="' + fieldInfo.form + '"';
278
433
  }
279
- if (fieldInfo.linkText) {
280
- value += ' text="' + fieldInfo.linkText + '"';
434
+ if (fieldInfo.linktab) {
435
+ value += ' linktab="' + fieldInfo.linktab + '"';
436
+ }
437
+ if (fieldInfo.linktext) {
438
+ value += ' text="' + fieldInfo.linktext + '"';
281
439
  }
282
440
  if (fieldInfo.readonly) {
283
- value += ' readonly="true"';
441
+ if (typeof fieldInfo.readonly === "boolean") {
442
+ value += " readonly=\"true\"";
443
+ }
444
+ else {
445
+ value += " ng-readonly=\"".concat(fieldInfo.readonly, "\"");
446
+ }
284
447
  }
285
448
  value += '></fng-link>';
286
449
  break;
287
450
  case 'radio':
288
451
  value = '';
289
- common += requiredStr + (fieldInfo.readonly ? ' disabled ' : ' ');
452
+ common += requiredStr;
453
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
454
+ ;
455
+ common += fieldInfo.add ? (' ' + fieldInfo.add + ' ') : '';
290
456
  var separateLines = options.formstyle === 'vertical' || (options.formstyle !== 'inline' && !fieldInfo.inlineRadio);
291
457
  if (angular.isArray(fieldInfo.options)) {
292
458
  if (options.subschema) {
@@ -296,8 +462,7 @@ var fng;
296
462
  var thisCommon_1;
297
463
  angular.forEach(fieldInfo.options, function (optValue, idx) {
298
464
  thisCommon_1 = common.replace('id="', 'id="' + idx + '-');
299
- value += '<input ' + thisCommon_1 + 'type="radio"';
300
- value += ' value="' + optValue + '">' + optValue;
465
+ value += "<input ".concat(thisCommon_1, " type=\"radio\" aria-label=\"").concat(optValue, "\" value=\"").concat(optValue, "\">").concat(optValue);
301
466
  if (separateLines) {
302
467
  value += '<br />';
303
468
  }
@@ -312,22 +477,23 @@ var fng;
312
477
  }
313
478
  enumInstruction = generateEnumInstructions();
314
479
  value += '<' + tagType + ' ng-repeat="option in ' + enumInstruction.repeat + '">';
315
- value += '<input ' + common.replace('id="', 'id="{{$index}}-') + ' type="radio" value="{{' + enumInstruction.value + '}}"> {{';
316
- value += enumInstruction.label || enumInstruction.value;
317
- value += ' }} </' + tagType + '> ';
480
+ value += "<input ".concat(common.replace('id="', 'id="{{$index}}-'), " type=\"radio\" aria-label=\"").concat(enumInstruction.value, "\" value=\"{{ ").concat(enumInstruction.value, " }}\"> {{ ").concat(enumInstruction.label || enumInstruction.value, " }} </").concat(tagType, "> ");
318
481
  }
319
482
  break;
320
483
  case 'checkbox':
321
- common += requiredStr + (fieldInfo.readonly ? ' disabled ' : ' ');
484
+ common += requiredStr;
485
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
486
+ ;
487
+ common += fieldInfo.add ? (' ' + fieldInfo.add + ' ') : '';
488
+ value = formMarkupHelper.generateSimpleInput(common, fieldInfo, options);
322
489
  if (cssFrameworkService.framework() === 'bs3') {
323
- value = '<div class="checkbox"><input ' + common + 'type="checkbox"></div>';
324
- }
325
- else {
326
- value = formMarkupHelper.generateSimpleInput(common, fieldInfo, options);
490
+ value = '<div class="checkbox">' + value + '</div>';
327
491
  }
328
492
  break;
329
493
  default:
330
494
  common += formMarkupHelper.addTextInputMarkup(allInputsVars, fieldInfo, requiredStr);
495
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
496
+ ;
331
497
  if (fieldInfo.type === 'textarea') {
332
498
  if (fieldInfo.rows) {
333
499
  if (fieldInfo.rows === 'auto') {
@@ -344,7 +510,7 @@ var fng;
344
510
  allInputsVars.sizeClassBS3 = 'col-xs-12';
345
511
  }
346
512
  }
347
- value = '<textarea ' + common + ' />';
513
+ value = '<textarea ' + common + '></textarea>';
348
514
  }
349
515
  else {
350
516
  value = formMarkupHelper.generateSimpleInput(common, fieldInfo, options);
@@ -364,6 +530,9 @@ var fng;
364
530
  case 'inline':
365
531
  result = 'form-inline';
366
532
  break;
533
+ case 'stacked':
534
+ result = 'form-stacked';
535
+ break;
367
536
  case 'horizontalCompact':
368
537
  result = 'form-horizontal compact';
369
538
  break;
@@ -374,11 +543,12 @@ var fng;
374
543
  return result;
375
544
  };
376
545
  var containerInstructions = function (info) {
377
- var result = { before: '', after: '' };
546
+ var result;
378
547
  if (typeof info.containerType === 'function') {
379
548
  result = info.containerType(info);
380
549
  }
381
550
  else {
551
+ result = {};
382
552
  switch (info.containerType) {
383
553
  case 'tab':
384
554
  var tabNo = -1;
@@ -391,12 +561,26 @@ var fng;
391
561
  if (tabNo >= 0) {
392
562
  // TODO Figure out tab history updates (check for other tab-history-todos)
393
563
  // result.before = '<uib-tab deselect="tabDeselect($event, $selectedIndex)" select="updateQueryForTab(\'' + info.title + '\')" heading="' + info.title + '"'
394
- result.before = '<uib-tab select="updateQueryForTab(\'' + info.title + '\')" heading="' + info.title + '"';
395
- if (tabNo > 0) {
396
- result.before += 'active="tabs[' + tabNo + '].active"';
564
+ var idStr = "".concat(_.camelCase(info.title), "Tab");
565
+ var visibility = securityService.considerVisibility(idStr, scope);
566
+ if (visibility.omit) {
567
+ // we already know this field should be invisible, so we needn't add anything for it
568
+ result.omit = true;
569
+ }
570
+ else {
571
+ var attrs_1 = "id=\"".concat(idStr, "\"");
572
+ if (visibility.visibilityAttr) {
573
+ // an angular expression to determine the visibility of this field later...
574
+ attrs_1 += " ".concat(visibility.visibilityAttr);
575
+ }
576
+ attrs_1 += securityService.generateDisabledAttr(idStr, scope, { attr: "disable", attrRequiresValue: true }); // uib-tab expects 'disable="true"` rather than 'disabled="true"' or just disabled
577
+ result.before = "<uib-tab ".concat(attrs_1, " deselect=\"tabDeselect($event, $selectedIndex)\" select=\"updateQueryForTab('").concat(info.title, "')\" heading=\"").concat(info.title, "\"");
578
+ if (tabNo > 0) {
579
+ result.before += 'active="tabs[' + tabNo + '].active"';
580
+ }
581
+ result.before += '>';
582
+ result.after = '</uib-tab>';
397
583
  }
398
- result.before += '>';
399
- result.after = '</uib-tab>';
400
584
  }
401
585
  else {
402
586
  result.before = '<p>Error! Tab ' + info.title + ' not found in tab list</p>';
@@ -452,8 +636,46 @@ var fng;
452
636
  }
453
637
  return result;
454
638
  };
639
+ var generateInlineHeaders = function (instructionsArray, options, model, evenWhenEmpty) {
640
+ // "column" headers for nested schemas that use formStyle: "inline" will only line up with their respective
641
+ // controls when widths are applied to both the cg_f_xxxx and col_label_xxxx element using css.
642
+ // Likely, the widths will need to be the same, so consider using the following:
643
+ // div[id$="_f_<collection>_<field>"] {
644
+ // width: 100px;
645
+ // }
646
+ // one column can grow to the remaining available width thus:
647
+ // div[id$="_f_<collection>_<field>"] {
648
+ // flex-grow: 1;
649
+ // }
650
+ var hideWhenEmpty = evenWhenEmpty ? "" : "ng-hide=\"!".concat(model, " || ").concat(model, ".length === 0\"");
651
+ var res = "<div class=\"inline-col-headers\" style=\"display:flex\" ".concat(hideWhenEmpty, ">");
652
+ for (var _i = 0, instructionsArray_1 = instructionsArray; _i < instructionsArray_1.length; _i++) {
653
+ var info = instructionsArray_1[_i];
654
+ // need to call this now to ensure the id is set. will probably be (harmlessly) called again later.
655
+ inferMissingProperties(info, options);
656
+ res += '<div ';
657
+ info.showWhen = info.showWhen || info.showwhen; // deal with use within a directive
658
+ if (info.showWhen) {
659
+ if (typeof info.showWhen === 'string') {
660
+ res += 'ng-show="' + info.showWhen + '"';
661
+ }
662
+ else {
663
+ res += 'ng-show="' + formMarkupHelper.generateNgShow(info.showWhen, model) + '"';
664
+ }
665
+ }
666
+ if (info.id && typeof info.id.replace === "function") {
667
+ res += ' id="col_label_' + info.id.replace(/\./g, '-') + '"';
668
+ }
669
+ res += " class=\"inline-col-header\"><label for=\"".concat(info.id, "\" class=\"control-label\">").concat(info.label, "</label></div>");
670
+ }
671
+ res += "</div>";
672
+ return res;
673
+ };
455
674
  var handleField = function (info, options) {
456
675
  var fieldChrome = formMarkupHelper.fieldChrome(scope, info, options);
676
+ if (fieldChrome.omit) {
677
+ return "";
678
+ }
457
679
  var template = fieldChrome.template;
458
680
  if (info.schema) {
459
681
  var niceName = info.name.replace(/\./g, '_');
@@ -474,7 +696,8 @@ var fng;
474
696
  formstyle: options.formstyle,
475
697
  subkey: schemaDefName + '_subkey',
476
698
  subkeyno: arraySel,
477
- subschemaroot: info.name
699
+ subschemaroot: info.name,
700
+ suppressNestingWarning: info.suppressNestingWarning
478
701
  });
479
702
  template += topAndTail.after;
480
703
  }
@@ -482,7 +705,9 @@ var fng;
482
705
  }
483
706
  else {
484
707
  if (options.subschema) {
485
- console.log('Attempts at supporting deep nesting have been removed - will hopefully be re-introduced at a later date');
708
+ if (!options.suppressNestingWarning) {
709
+ console.log('Attempts at supporting deep nesting have been removed - will hopefully be re-introduced at a later date');
710
+ }
486
711
  }
487
712
  else {
488
713
  var model = (options.model || 'record') + '.' + info.name;
@@ -504,20 +729,47 @@ var fng;
504
729
  }
505
730
  }
506
731
  /* Array body */
507
- template += '<ol class="sub-doc"' + (info.sortable ? " ui-sortable=\"sortableOptions\" ng-model=\"" + model + "\"" : '') + '>';
508
- template += '<li ng-form class="' + (cssFrameworkService.framework() === 'bs2' ? 'row-fluid ' : '') +
509
- convertFormStyleToClass(info.formStyle) + '" name="form_' + niceName + '{{$index}}" class="sub-doc well" id="' + info.id + 'List_{{$index}}" ' +
510
- ' ng-repeat="subDoc in ' + model + ' track by $index">';
732
+ if (info.formStyle === "inline" && info.inlineHeaders) {
733
+ template += generateInlineHeaders(info.schema, options, model, info.inlineHeaders === "always");
734
+ }
735
+ // handleReadOnlyDisabled() returns two strings - the 'disabled' attribute(s), and the 'disableable'
736
+ // attributes. for the purpose of deciding if / how to disable sorting if the list itself is
737
+ // disabled, we're only interested in the former...
738
+ var disableCond = formMarkupHelper.handleReadOnlyDisabled(info, scope)[0];
739
+ // if we already know that the field is disabled (only possible when formsAngular.elemSecurityFuncBinding === "instant")
740
+ // then we don't need to add the sortable attribute at all
741
+ // otherwise, we need to include the ng-disabled on the <ol> so this can be referenced by the ui-sortable directive
742
+ // (see sortableOptions)
743
+ var sortableStr = info.sortable && disableCond.trim().toLowerCase() !== "disabled"
744
+ ? "".concat(disableCond, " ui-sortable=\"sortableOptions\" ng-model=\"").concat(model, "\"")
745
+ : "";
746
+ template += "<ol class=\"sub-doc\" ".concat(sortableStr, ">");
747
+ // if a "disabled + children" DOM effect is applied to the list, this should serve to disable all of the
748
+ // fields in the list sub-schema, along with the remove and add buttons for this list. the following
749
+ // string will be added to the list items and the add and remove buttons to identify this fact.
750
+ var disableableAncestorStr = formMarkupHelper.genDisableableAncestorStr(info.id);
751
+ template +=
752
+ "<li ng-form id=\"".concat(info.id, "List_{{$index}}\" name=\"form_").concat(niceName, "{{$index}}\" ").concat(disableableAncestorStr) +
753
+ " class=\"".concat(convertFormStyleToClass(info.formStyle)) +
754
+ " ".concat(cssFrameworkService.framework() === 'bs2' ? 'row-fluid' : '') +
755
+ " ".concat(info.inlineHeaders ? 'width-controlled' : '') +
756
+ " ".concat(info.ngClass ? "ng-class:" + info.ngClass : '', "\"") +
757
+ " ng-repeat=\"subDoc in ".concat(model, " track by $index\"") +
758
+ " ".concat(info.filterable ? 'data-ng-hide="subDoc._hidden"' : '') +
759
+ ">";
511
760
  if (cssFrameworkService.framework() === 'bs2') {
512
761
  template += '<div class="row-fluid sub-doc">';
513
762
  }
514
- if (!info.noRemove || info.customSubDoc) {
515
- template += ' <div class="sub-doc-btns">';
763
+ if (info.noRemove !== true || info.customSubDoc) {
764
+ // we need to put disableableAncestorStr on the div rather than on the remove button because the
765
+ // way that is styled means that any coloured outlines that might be added when "Identify page elements" is on
766
+ // will be masked
767
+ template += " <div class=\"sub-doc-btns\" ".concat(info.noRemove !== true ? disableableAncestorStr : "", ">");
516
768
  if (typeof info.customSubDoc == 'string') {
517
769
  template += info.customSubDoc;
518
770
  }
519
771
  if (info.noRemove !== true) {
520
- template += "<button " + (info.noRemove ? 'ng-hide="' + info.noRemove + '"' : '') + " name=\"remove_" + info.id + "_btn\" ng-click=\"remove('" + info.name + "', $index, $event)\"";
772
+ template += "<button ".concat(disableCond, " ").concat(info.noRemove ? 'ng-hide="' + info.noRemove + '"' : '', " name=\"remove_").concat(info.id, "_btn\" ng-click=\"remove('").concat(info.name, "', $index, $event)\"");
521
773
  if (info.remove) {
522
774
  template += ' class="remove-btn btn btn-mini btn-default btn-xs form-btn"><i class="' + formMarkupHelper.glyphClass() + '-minus"></i> Remove';
523
775
  }
@@ -534,31 +786,69 @@ var fng;
534
786
  }
535
787
  template += '</div> ';
536
788
  }
789
+ var parts = void 0;
790
+ if (info.subDocContainerType) {
791
+ var containerType = scope[info.subDocContainerType] || info.subDocContainerType;
792
+ var containerProps = Object.assign({ containerType: containerType }, info.subDocContainerProps);
793
+ parts = containerInstructions(containerProps);
794
+ }
795
+ if (parts === null || parts === void 0 ? void 0 : parts.before) {
796
+ template += parts.before;
797
+ }
537
798
  template += processInstructions(info.schema, false, {
538
799
  subschema: 'true',
539
800
  formstyle: info.formStyle,
540
801
  model: options.model,
541
- subschemaroot: info.name
802
+ subschemaroot: info.name,
803
+ suppressNestingWarning: info.suppressNestingWarning
542
804
  });
805
+ if (parts === null || parts === void 0 ? void 0 : parts.after) {
806
+ template += parts.after;
807
+ }
543
808
  if (cssFrameworkService.framework() === 'bs2') {
544
809
  template += ' </div>';
545
810
  }
546
811
  template += '</li>';
547
812
  template += '</ol>';
548
813
  /* Array footer */
549
- if (info.noAdd !== true || typeof info.customFooter == 'string') {
814
+ if (info.noAdd !== true || typeof info.customFooter == 'string' || info.noneIndicator) {
550
815
  var footer = '';
551
816
  if (typeof info.customFooter == 'string') {
552
817
  footer = info.customFooter;
553
818
  }
554
- if (info.noAdd !== true) {
555
- footer += "<button " + (info.noAdd ? 'ng-hide="' + info.noAdd + '"' : '') + " id=\"add_" + info.id + "_btn\" class=\"add-btn btn btn-default btn-xs btn-mini\" ng-click=\"add('" + info.name + "',$event)\">\n <i class=\"' + formMarkupHelper.glyphClass() + '-plus\"></i> \n Add\n </button>";
556
- }
557
- if (cssFrameworkService.framework() === 'bs3') {
558
- template += '<div class="row schema-foot"><div class="col-sm-offset-3">' + footer + '</div></div>';
819
+ var hideCond = '';
820
+ var indicatorShowCond = "".concat(model, ".length == 0");
821
+ if (info.noAdd === true) {
822
+ indicatorShowCond = "ng-show=\"".concat(indicatorShowCond, "\"");
559
823
  }
560
824
  else {
561
- template += '<div class = "schema-foot ">' + footer + '</div>';
825
+ hideCond = info.noAdd ? "ng-hide=\"".concat(info.noAdd, "\"") : '';
826
+ indicatorShowCond = info.noAdd ? "ng-show=\"".concat(info.noAdd, " && ").concat(indicatorShowCond, "\"") : '';
827
+ // we need the button to have disableCond (to actually disable it, if the list is disabled)
828
+ // adding disableableAncestorStr seems more correct than for it to have the disableable attribute
829
+ footer +=
830
+ "<button ".concat(hideCond, " ").concat(disableCond, " ").concat(disableableAncestorStr, " id=\"add_").concat(info.id, "_btn\" class=\"add-btn btn btn-default btn-xs btn-mini\" ng-click=\"add('").concat(info.name, "',$event)\">") +
831
+ " <i class=\"".concat(formMarkupHelper.glyphClass(), "-plus\"></i> Add ") +
832
+ "</button>";
833
+ }
834
+ if (info.noneIndicator) {
835
+ footer += "<span ".concat(indicatorShowCond, " class=\"none_indicator\" id=\"no_").concat(info.id, "_indicator\">None</span>");
836
+ // hideCond for the schema-foot is if there's no add button and no indicator
837
+ hideCond = "".concat(model, ".length > 0");
838
+ if (info.noAdd === true) {
839
+ hideCond = "ng-hide=\"".concat(hideCond, "\"");
840
+ }
841
+ else {
842
+ hideCond = info.noAdd ? "ng-hide=\"".concat(info.noAdd, " && ").concat(hideCond, "\"") : '';
843
+ }
844
+ }
845
+ if (footer !== '') {
846
+ if (cssFrameworkService.framework() === 'bs3') {
847
+ template += "<div ".concat(hideCond, " class=\"row schema-foot\"><div class=\"col-sm-offset-3\">").concat(footer, "</div></div>");
848
+ }
849
+ else {
850
+ template += "<div ".concat(hideCond, " class = \"schema-foot \">").concat(footer, "</div>");
851
+ }
562
852
  }
563
853
  }
564
854
  }
@@ -570,34 +860,40 @@ var fng;
570
860
  var controlDivClasses = formMarkupHelper.controlDivClasses(options);
571
861
  if (info.array) {
572
862
  controlDivClasses.push('fng-array');
573
- if (options.formstyle === 'inline') {
574
- throw new Error('Cannot use arrays in an inline form');
863
+ if (options.formstyle === 'inline' || options.formstyle === 'stacked') {
864
+ throw new Error('Cannot use arrays in an inline or stacked form');
575
865
  }
576
866
  template += formMarkupHelper.label(scope, info, info.type !== 'link', options);
577
- template += formMarkupHelper.handleArrayInputAndControlDiv(generateInput(info, info.type === 'link' ? null : 'arrayItem.x', true, info.id + '_{{$index}}', options), controlDivClasses, info, options);
867
+ var stashedHelp = info.help;
868
+ delete info.help;
869
+ var inputHtml = generateInput(info, info.type === 'link' ? null : 'arrayItem.x', true, options);
870
+ info.help = stashedHelp;
871
+ template += formMarkupHelper.handleArrayInputAndControlDiv(inputHtml, controlDivClasses, scope, info, options);
578
872
  }
579
873
  else {
580
874
  // Single fields here
581
875
  template += formMarkupHelper.label(scope, info, null, options);
582
- if (options.required) {
583
- console.log("********* Options required - found it ********");
584
- }
585
- template += formMarkupHelper.handleInputAndControlDiv(generateInput(info, null, options.required, info.id, options), controlDivClasses);
876
+ template += formMarkupHelper.handleInputAndControlDiv(generateInput(info, null, options.required, options), controlDivClasses);
586
877
  }
587
878
  }
588
879
  template += fieldChrome.closeTag;
589
880
  return template;
590
881
  };
591
- var inferMissingProperties = function (info) {
882
+ var inferMissingProperties = function (info, options) {
592
883
  // infer missing values
593
884
  info.type = info.type || 'text';
594
885
  if (info.id) {
595
- if (typeof info.id === 'number' || (info.id[0] >= 0 && info.id <= '9')) {
886
+ if (typeof info.id === 'number' || info.id.match(/^[0-9]/)) {
596
887
  info.id = '_' + info.id;
597
888
  }
598
889
  }
599
890
  else {
600
- info.id = 'f_' + info.name.replace(/\./g, '_');
891
+ if (options && options.noid) {
892
+ info.id = null;
893
+ }
894
+ else {
895
+ info.id = 'f_' + info.name.replace(/\./g, '_');
896
+ }
601
897
  }
602
898
  info.label = (info.label !== undefined) ? (info.label === null ? '' : info.label) : $filter('titleCase')(info.name.split('.').slice(-1)[0]);
603
899
  };
@@ -608,7 +904,11 @@ var fng;
608
904
  if (instructionsArray) {
609
905
  for (var anInstruction = 0; anInstruction < instructionsArray.length; anInstruction++) {
610
906
  var info = instructionsArray[anInstruction];
611
- if (anInstruction === 0 && topLevel && !options.schema.match(/$_schema_/) && typeof info.add !== 'object') {
907
+ if (options.viewform) {
908
+ info = angular.copy(info);
909
+ info.readonly = true;
910
+ }
911
+ if (anInstruction === 0 && topLevel && !options.schema.match(/\$_schema_/) && typeof info.add !== 'object') {
612
912
  info.add = info.add ? ' ' + info.add + ' ' : '';
613
913
  if (info.add.indexOf('ui-date') === -1 && !options.noautofocus && !info.containerType) {
614
914
  info.add = info.add + 'autofocus ';
@@ -617,9 +917,10 @@ var fng;
617
917
  var callHandleField = true;
618
918
  if (info.directive) {
619
919
  var directiveName = info.directive;
620
- var newElement = '<' + directiveName + ' model="' + (options.model || 'record') + '"';
920
+ var newElement = info.customHeader || "";
921
+ newElement += '<' + directiveName + ' model="' + (options.model || 'record') + '"';
621
922
  var thisElement = element[0];
622
- inferMissingProperties(info);
923
+ inferMissingProperties(info, options);
623
924
  for (var i = 0; i < thisElement.attributes.length; i++) {
624
925
  var thisAttr = thisElement.attributes[i];
625
926
  switch (thisAttr.nodeName) {
@@ -655,7 +956,9 @@ var fng;
655
956
  break;
656
957
  case 'object':
657
958
  for (var subAdd in info.add) {
658
- newElement += ' ' + subAdd + '="' + info.add[subAdd].toString().replace(/"/g, '&quot;') + '"';
959
+ if (info.add.hasOwnProperty(subAdd)) {
960
+ newElement += ' ' + subAdd + '="' + info.add[subAdd].toString().replace(/"/g, '&quot;') + '"';
961
+ }
659
962
  }
660
963
  break;
661
964
  default:
@@ -664,13 +967,21 @@ var fng;
664
967
  break;
665
968
  case directiveCamel:
666
969
  for (var subProp in info[prop]) {
667
- newElement += info.directive + '-' + subProp + '="' + info[prop][subProp] + '"';
970
+ if (info[prop].hasOwnProperty(subProp)) {
971
+ newElement += " ".concat(info.directive, "-").concat(subProp, "=\"");
972
+ if (typeof info[prop][subProp] === 'string') {
973
+ newElement += "".concat(info[prop][subProp].replace(/"/g, '&quot;'), "\"");
974
+ }
975
+ else {
976
+ newElement += "".concat(JSON.stringify(info[prop][subProp]).replace(/"/g, '&quot;'), "\"");
977
+ }
978
+ }
668
979
  }
669
980
  break;
670
981
  default:
671
982
  if (info[prop]) {
672
983
  if (typeof info[prop] === 'string') {
673
- newElement += ' fng-fld-' + prop + '="' + info[prop].toString().replace(/"/g, '&quot;') + '"';
984
+ newElement += ' fng-fld-' + prop + '="' + info[prop].replace(/"/g, '&quot;') + '"';
674
985
  }
675
986
  else {
676
987
  newElement += ' fng-fld-' + prop + '="' + JSON.stringify(info[prop]).replace(/"/g, '&quot;') + '"';
@@ -686,10 +997,13 @@ var fng;
686
997
  }
687
998
  }
688
999
  newElement += 'ng-model="' + info.name + '"></' + directiveName + '>';
1000
+ newElement += (info.customFooter || "");
689
1001
  result += newElement;
690
1002
  callHandleField = false;
691
1003
  }
692
1004
  else if (info.containerType) {
1005
+ // for now, the following call will only consider security for tabs and not other container types.
1006
+ // hence why we...
693
1007
  var parts = containerInstructions(info);
694
1008
  switch (info.containerType) {
695
1009
  case 'tab':
@@ -700,9 +1014,12 @@ var fng;
700
1014
  var activeTabNo = _.findIndex(scope.tabs, function (tab) { return (tab.active); });
701
1015
  scope.activeTabNo = activeTabNo >= 0 ? activeTabNo : 0;
702
1016
  }
703
- result += parts.before;
704
- result += processInstructions(info.content, null, options);
705
- result += parts.after;
1017
+ // ...only check for this here!
1018
+ if (!parts.omit) {
1019
+ result += parts.before;
1020
+ result += processInstructions(info.content, null, options);
1021
+ result += parts.after;
1022
+ }
706
1023
  break;
707
1024
  case 'tabset':
708
1025
  tabsSetup = tabsSetupState.Y;
@@ -722,9 +1039,7 @@ var fng;
722
1039
  else if (options.subkey) {
723
1040
  // Don't display fields that form part of the subkey, as they should not be edited (because in these circumstances they form some kind of key)
724
1041
  var objectToSearch = angular.isArray(scope[options.subkey]) ? scope[options.subkey][0].keyList : scope[options.subkey].keyList;
725
- if (_.find(objectToSearch, function (value, key) {
726
- return scope[options.subkey].path + '.' + key === info.name;
727
- })) {
1042
+ if (_.find(objectToSearch, function (value, key) { return scope[options.subkey].path + '.' + key === info.name; })) {
728
1043
  callHandleField = false;
729
1044
  }
730
1045
  }
@@ -732,7 +1047,7 @@ var fng;
732
1047
  // if (groupId) {
733
1048
  // scope['showHide' + groupId] = true;
734
1049
  // }
735
- inferMissingProperties(info);
1050
+ inferMissingProperties(info, options);
736
1051
  result += handleField(info, options);
737
1052
  }
738
1053
  }
@@ -746,8 +1061,9 @@ var fng;
746
1061
  var unwatch = scope.$watch(attrs.schema, function (newValue) {
747
1062
  if (newValue) {
748
1063
  var newArrayValue = angular.isArray(newValue) ? newValue : [newValue]; // otherwise some old tests stop working for no real reason
749
- if (newArrayValue.length > 0) {
1064
+ if (newArrayValue.length > 0 && typeof unwatch === "function") {
750
1065
  unwatch();
1066
+ unwatch = null;
751
1067
  var elementHtml = '';
752
1068
  var recordAttribute = attrs.model || 'record'; // By default data comes from scope.record
753
1069
  var theRecord = scope[recordAttribute];
@@ -761,12 +1077,13 @@ var fng;
761
1077
  var customAttrs = '';
762
1078
  for (var thisAttr in attrs) {
763
1079
  if (attrs.hasOwnProperty(thisAttr)) {
764
- if (thisAttr[0] !== '$' && ['name', 'formstyle', 'schema', 'subschema', 'model'].indexOf(thisAttr) === -1) {
1080
+ if (thisAttr[0] !== '$' && ['name', 'formstyle', 'schema', 'subschema', 'model', 'viewform'].indexOf(thisAttr) === -1) {
765
1081
  customAttrs += ' ' + attrs.$attr[thisAttr] + '="' + attrs[thisAttr] + '"';
766
1082
  }
767
1083
  }
768
1084
  }
769
- elementHtml = '<form name="' + scope.topLevelFormName + '" class="' + convertFormStyleToClass(attrs.formstyle) + ' novalidate"' + customAttrs + '>';
1085
+ var tag = attrs.forceform ? 'ng-form' : 'form';
1086
+ elementHtml = "<".concat(tag, " name=\"").concat(scope.topLevelFormName, "\" class=\"").concat(convertFormStyleToClass(attrs.formstyle), "\" novalidate ").concat(customAttrs, ">");
770
1087
  }
771
1088
  if (theRecord === scope.topLevelFormName) {
772
1089
  throw new Error('Model and Name must be distinct - they are both ' + theRecord);
@@ -782,10 +1099,12 @@ var fng;
782
1099
  // If we have modelControllers we need to let them know when we have form + data
783
1100
  var sharedData = scope[attrs.shared || 'sharedData'];
784
1101
  var modelControllers_1 = sharedData ? sharedData.modelControllers : [];
785
- if (subkeys.length > 0 || modelControllers_1.length > 0) {
1102
+ if ((subkeys.length > 0 || modelControllers_1.length > 0) && !scope.phaseWatcher) {
786
1103
  var unwatch2 = scope.$watch('phase', function (newValue) {
787
- if (newValue === 'ready') {
1104
+ scope.phaseWatcher = true;
1105
+ if (newValue === 'ready' && typeof unwatch2 === "function") {
788
1106
  unwatch2();
1107
+ unwatch2 = null;
789
1108
  // Tell the 'model controllers' that the form and data are there
790
1109
  for (var i = 0; i < modelControllers_1.length; i++) {
791
1110
  if (modelControllers_1[i].onAllReady) {
@@ -814,7 +1133,7 @@ var fng;
814
1133
  arrayOffset = scope[arrayToProcess[thisOffset].selectFunc](theRecord, info);
815
1134
  }
816
1135
  else if (arrayToProcess[thisOffset].keyList) {
817
- // We are chosing the array element by matching one or more keys
1136
+ // We are choosing the array element by matching one or more keys
818
1137
  var thisSubkeyList = arrayToProcess[thisOffset].keyList;
819
1138
  for (arrayOffset = 0; arrayOffset < dataVal.length; arrayOffset++) {
820
1139
  matching = true;
@@ -868,7 +1187,7 @@ var fng;
868
1187
  }
869
1188
  });
870
1189
  }
871
- $rootScope.$broadcast('formInputDone');
1190
+ $rootScope.$broadcast('formInputDone', attrs.name);
872
1191
  if (formGenerator.updateDataDependentDisplay && theRecord && Object.keys(theRecord).length > 0) {
873
1192
  // If this is not a test force the data dependent updates to the DOM
874
1193
  formGenerator.updateDataDependentDisplay(theRecord, null, true, scope);
@@ -917,7 +1236,7 @@ var fng;
917
1236
  /*@ngInject*/
918
1237
  SearchCtrl.$inject = ["$scope", "$http", "$location", "routingService"];
919
1238
  function SearchCtrl($scope, $http, $location, routingService) {
920
- var currentRequest = '';
1239
+ var lastSearchSent;
921
1240
  var _isNotMobile;
922
1241
  _isNotMobile = (function () {
923
1242
  var check = false;
@@ -957,7 +1276,7 @@ var fng;
957
1276
  break;
958
1277
  case 13:
959
1278
  if ($scope.focus != null) {
960
- $scope.selectResult($scope.focus);
1279
+ $location.url(makeUrlNoHtml5Hash($scope.results[$scope.focus]));
961
1280
  }
962
1281
  break;
963
1282
  }
@@ -970,14 +1289,6 @@ var fng;
970
1289
  $scope.results[index].focussed = true;
971
1290
  $scope.focus = index;
972
1291
  };
973
- $scope.selectResult = function (resultNo) {
974
- var result = $scope.results[resultNo];
975
- var newURL = routingService.prefix() + '/' + result.resource + '/' + result.id + '/edit';
976
- if (result.resourceTab) {
977
- newURL += '/' + result.resourceTab;
978
- }
979
- $location.url(newURL);
980
- };
981
1292
  $scope.resultClass = function (index) {
982
1293
  var resultClass = 'search-result';
983
1294
  if ($scope.results && $scope.results[index].focussed) {
@@ -991,16 +1302,23 @@ var fng;
991
1302
  $scope.results = [];
992
1303
  $scope.focus = null;
993
1304
  };
1305
+ function makeUrlNoHtml5Hash(result) {
1306
+ return result.url ? routingService.html5hash() + result.url.replace('|id|', result.id) :
1307
+ routingService.buildOperationUrl('edit', result.resource, undefined, result.id, result.resourceTab);
1308
+ }
994
1309
  $scope.$watch('searchTarget', function (newValue) {
995
1310
  if (newValue && newValue.length > 0) {
996
- currentRequest = newValue;
997
- $http.get('/api/search?q=' + newValue).then(function (response) {
1311
+ lastSearchSent = $scope.testTime || new Date().valueOf();
1312
+ $http.get('/api/search?q=' + newValue + '&sentAt=' + lastSearchSent).then(function (response) {
998
1313
  var data = response.data;
999
1314
  // Check that we haven't fired off a subsequent request, in which
1000
1315
  // case we are no longer interested in these results
1001
- if (currentRequest === newValue) {
1316
+ if (!data.timestamps || !data.timestamps.sentAt || Number.parseInt(data.timestamps.sentAt) === lastSearchSent) {
1002
1317
  if ($scope.searchTarget.length > 0) {
1003
1318
  $scope.results = data.results;
1319
+ $scope.results.forEach(function (result) {
1320
+ result.href = routingService.html5hash() + makeUrlNoHtml5Hash(result);
1321
+ });
1004
1322
  $scope.moreCount = data.moreCount;
1005
1323
  if (data.results.length > 0) {
1006
1324
  $scope.errorClass = '';
@@ -1065,6 +1383,21 @@ var fng;
1065
1383
  })(fng || (fng = {}));
1066
1384
  /// <reference path="../../../../node_modules/@types/angular/index.d.ts" />
1067
1385
  var fng;
1386
+ (function (fng) {
1387
+ var filters;
1388
+ (function (filters) {
1389
+ /*@ngInject*/
1390
+ function extractTimestampFromMongoID() {
1391
+ return function (id) {
1392
+ var timestamp = id.substring(0, 8);
1393
+ return new Date(parseInt(timestamp, 16) * 1000);
1394
+ };
1395
+ }
1396
+ filters.extractTimestampFromMongoID = extractTimestampFromMongoID;
1397
+ })(filters = fng.filters || (fng.filters = {}));
1398
+ })(fng || (fng = {}));
1399
+ /// <reference path="../../../../node_modules/@types/angular/index.d.ts" />
1400
+ var fng;
1068
1401
  (function (fng) {
1069
1402
  var filters;
1070
1403
  (function (filters) {
@@ -1245,6 +1578,7 @@ var fng;
1245
1578
  controllerName += 'Ctrl';
1246
1579
  locals.$scope = sharedData.modelControllers[level] = localScope;
1247
1580
  var parentScope = localScope.$parent;
1581
+ parentScope.items = parentScope.items || [];
1248
1582
  var addMenuOptions = function (array) {
1249
1583
  angular.forEach(array, function (value) {
1250
1584
  if (value.divider) {
@@ -1255,6 +1589,10 @@ var fng;
1255
1589
  needDivider = false;
1256
1590
  parentScope.items.push({ divider: true });
1257
1591
  }
1592
+ if (!value.id) {
1593
+ // if it doesn't have an id, give it one, so every menu item is possible to secure
1594
+ value.id = _.camelCase(value.text || value.textFunc() || "");
1595
+ }
1258
1596
  parentScope.items.push(value);
1259
1597
  }
1260
1598
  });
@@ -1276,6 +1614,11 @@ var fng;
1276
1614
  locals.$scope.contextMenuPromise.then(function (array) { return addMenuOptions(array); });
1277
1615
  }
1278
1616
  }
1617
+ if (sharedData.modelNameDisplayPromise) {
1618
+ sharedData.modelNameDisplayPromise.then(function (value) {
1619
+ parentScope.modelNameDisplay = value;
1620
+ });
1621
+ }
1279
1622
  }
1280
1623
  catch (error) {
1281
1624
  // Check to see if error is no such controller - don't care
@@ -1305,7 +1648,7 @@ var fng;
1305
1648
  routing: 'ngroute',
1306
1649
  prefix: '' // How do we want to prefix our routes? If not empty string then first character must be slash (which is added if not)
1307
1650
  };
1308
- var postActions = ['edit'];
1651
+ var postActions = ['edit', 'view'];
1309
1652
  var builtInRoutes = [
1310
1653
  {
1311
1654
  route: '/analyse/:model/:reportSchemaName',
@@ -1315,12 +1658,19 @@ var fng;
1315
1658
  { route: '/analyse/:model', state: 'analyse::model', templateUrl: 'base-analysis.html' },
1316
1659
  { route: '/:model/:id/edit', state: 'model::edit', templateUrl: 'base-edit.html' },
1317
1660
  { route: '/:model/:id/edit/:tab', state: 'model::edit::tab', templateUrl: 'base-edit.html' },
1661
+ { route: '/:model/:id/view', state: 'model::edit', templateUrl: 'base-view.html' },
1662
+ { route: '/:model/:id/view/:tab', state: 'model::view::tab', templateUrl: 'base-view.html' },
1318
1663
  { route: '/:model/new', state: 'model::new', templateUrl: 'base-edit.html' },
1319
1664
  { route: '/:model', state: 'model::list', templateUrl: 'base-list.html' },
1665
+ { route: '/:model/viewonly', state: 'model::view', templateUrl: 'base-list-view.html' },
1666
+ // Non default form (subset of fields etc)
1320
1667
  { route: '/:model/:form/:id/edit', state: 'model::form::edit', templateUrl: 'base-edit.html' },
1321
1668
  { route: '/:model/:form/:id/edit/:tab', state: 'model::form::edit::tab', templateUrl: 'base-edit.html' },
1669
+ { route: '/:model/:form/:id/view', state: 'model::form::view', templateUrl: 'base-view.html' },
1670
+ { route: '/:model/:form/:id/view/:tab', state: 'model::form::view::tab', templateUrl: 'base-view.html' },
1322
1671
  { route: '/:model/:form/new', state: 'model::form::new', templateUrl: 'base-edit.html' },
1323
- { route: '/:model/:form', state: 'model::form::list', templateUrl: 'base-list.html' } // list page with links to non default form
1672
+ { route: '/:model/:form', state: 'model::form::list', templateUrl: 'base-list.html' },
1673
+ { route: '/:model/:form/viewonly', state: 'model::form::list::view', templateUrl: 'base-list-view.html' } // list page with edit links to non default form
1324
1674
  ];
1325
1675
  var _routeProvider, _stateProvider;
1326
1676
  var lastRoute = null;
@@ -1374,6 +1724,12 @@ var fng;
1374
1724
  case 'edit':
1375
1725
  urlStr = modelString + formString + '/' + id + '/edit' + tabString;
1376
1726
  break;
1727
+ case 'view':
1728
+ urlStr = modelString + formString + '/' + id + '/view' + tabString;
1729
+ break;
1730
+ case 'read':
1731
+ urlStr = modelString + formString + '/' + id + '/read' + tabString;
1732
+ break;
1377
1733
  case 'new':
1378
1734
  urlStr = modelString + formString + '/new' + tabString;
1379
1735
  break;
@@ -1456,8 +1812,9 @@ var fng;
1456
1812
  lastObject.modelName = locationSplit[1];
1457
1813
  var lastParts_1 = [locationSplit[locationParts - 1], locationSplit[locationParts - 2]];
1458
1814
  var newPos = lastParts_1.indexOf('new');
1815
+ var viewonlyPos = lastParts_1.indexOf('viewonly');
1459
1816
  var actionPos = void 0;
1460
- if (newPos === -1) {
1817
+ if (newPos === -1 && viewonlyPos === -1) {
1461
1818
  actionPos = postActions.reduce(function (previousValue, currentValue) {
1462
1819
  var pos = lastParts_1.indexOf(currentValue);
1463
1820
  return pos > -1 ? pos : previousValue;
@@ -1467,10 +1824,13 @@ var fng;
1467
1824
  lastObject.id = locationSplit[locationParts];
1468
1825
  }
1469
1826
  }
1470
- else {
1827
+ else if (newPos !== -1) {
1471
1828
  lastObject.newRecord = true;
1472
1829
  locationParts -= (1 + newPos);
1473
1830
  }
1831
+ else {
1832
+ locationParts -= (1 + viewonlyPos);
1833
+ }
1474
1834
  if (actionPos === 1 || newPos === 1) {
1475
1835
  lastObject.tab = lastParts_1[0];
1476
1836
  }
@@ -1481,34 +1841,9 @@ var fng;
1481
1841
  }
1482
1842
  return lastObject;
1483
1843
  };
1484
- ///**
1485
- // * DominicBoettger wrote:
1486
- // *
1487
- // * Parser for the states provided by ui.router
1488
- // */
1489
- //'use strict';
1490
- //formsAngular.factory('$stateParse', [function () {
1491
- //
1492
- // var lastObject = {};
1493
- //
1494
- // return function (state) {
1495
- // if (state.current && state.current.name) {
1496
- // lastObject = {newRecord: false};
1497
- // lastObject.modelName = state.params.model;
1498
- // if (state.current.name === 'model::list') {
1499
- // lastObject = {index: true};
1500
- // lastObject.modelName = state.params.model;
1501
- // } else if (state.current.name === 'model::edit') {
1502
- // lastObject.id = state.params.id;
1503
- // } else if (state.current.name === 'model::new') {
1504
- // lastObject.newRecord = true;
1505
- // } else if (state.current.name === 'model::analyse') {
1506
- // lastObject.analyse = true;
1507
- // }
1508
- // }
1509
- // return lastObject;
1510
- // };
1511
- //}]);
1844
+ },
1845
+ html5hash: function () {
1846
+ return config.html5Mode ? '' : '#';
1512
1847
  },
1513
1848
  buildUrl: function (path) {
1514
1849
  var url = config.html5Mode ? '' : '#';
@@ -1525,46 +1860,25 @@ var fng;
1525
1860
  },
1526
1861
  redirectTo: function () {
1527
1862
  return function (operation, scope, location, id, tab) {
1528
- // switch (config.routing) {
1529
- // case 'ngroute' :
1530
- if (location.search()) {
1531
- location.url(location.path());
1863
+ location.search({}); // Lose any search parameters
1864
+ var urlStr;
1865
+ if (operation === 'onDelete') {
1866
+ if (config.onDelete) {
1867
+ if (config.onDelete === 'new') {
1868
+ urlStr = _buildOperationUrl(config.prefix, 'new', scope.modelName, scope.formName, id, tab);
1869
+ }
1870
+ else {
1871
+ urlStr = config.onDelete;
1872
+ }
1873
+ }
1874
+ else {
1875
+ urlStr = _buildOperationUrl(config.prefix, 'list', scope.modelName, scope.formName, id, tab);
1876
+ }
1877
+ }
1878
+ else {
1879
+ urlStr = _buildOperationUrl(config.prefix, operation, scope.modelName, scope.formName, id, tab);
1532
1880
  }
1533
- var urlStr = _buildOperationUrl(config.prefix, operation, scope.modelName, scope.formName, id, tab);
1534
1881
  location.path(urlStr);
1535
- // break;
1536
- // case 'uirouter' :
1537
- // var formString = scope.formName ? ('/' + scope.formName) : '';
1538
- // var modelString = config.prefix + '/' + scope.modelName;
1539
- // console.log('form schemas not supported with ui-router');
1540
- // switch (operation) {
1541
- // case 'list' :
1542
- // location.path(modelString + formString);
1543
- // break;
1544
- // case 'edit' :
1545
- // location.path(modelString + formString + '/' + id + '/edit');
1546
- // break;
1547
- // case 'new' :
1548
- // location.path(modelString + formString + '/new');
1549
- // break;
1550
- // }
1551
- // switch (operation) {
1552
- // case 'list' :
1553
- // $state.go('model::list', { model: model });
1554
- // break;
1555
- // case 'edit' :
1556
- // location.path('/' + scope.modelName + formString + '/' + id + '/edit');
1557
- // break;
1558
- // case 'new' :
1559
- // location.path('/' + scope.modelName + formString + '/new');
1560
- // break;
1561
- // }
1562
- // break;
1563
- //
1564
- //
1565
- // // edit: $state.go('model::edit', {id: data._id, model: $scope.modelName });
1566
- // // new: $state.go('model::new', {model: $scope.modelName});
1567
- // break;
1568
1882
  };
1569
1883
  }
1570
1884
  };
@@ -1586,7 +1900,7 @@ var fng;
1586
1900
  * All methods should be state-less
1587
1901
  *
1588
1902
  */
1589
- function formGenerator($location, $timeout, $filter, SubmissionsService, routingService, recordHandler) {
1903
+ function formGenerator($filter, routingService, recordHandler, securityService) {
1590
1904
  function handleSchema(description, source, destForm, destList, prefix, doRecursion, $scope, ctrlState) {
1591
1905
  function handletabInfo(tabName, thisInst) {
1592
1906
  var tabTitle = angular.copy(tabName);
@@ -1612,50 +1926,55 @@ var fng;
1612
1926
  }
1613
1927
  tab.content.push(thisInst);
1614
1928
  }
1929
+ if (typeof $scope.onSchemaFetch === "function") {
1930
+ $scope.onSchemaFetch(description, source);
1931
+ }
1615
1932
  for (var field in source) {
1616
- if (field === '_id') {
1617
- if (destList && source['_id'].options && source['_id'].options.list) {
1618
- handleListInfo(destList, source['_id'].options.list, field);
1619
- }
1620
- }
1621
- else if (source.hasOwnProperty(field)) {
1622
- var mongooseType = source[field], mongooseOptions = mongooseType.options || {};
1623
- var formData = mongooseOptions.form || {};
1624
- if (mongooseType.schema && !formData.hidden) {
1625
- if (doRecursion && destForm) {
1626
- var schemaSchema = [];
1627
- handleSchema('Nested ' + field, mongooseType.schema, schemaSchema, null, field + '.', true, $scope, ctrlState);
1628
- var sectionInstructions = basicInstructions(field, formData, prefix);
1629
- sectionInstructions.schema = schemaSchema;
1630
- if (formData.tab) {
1631
- handletabInfo(formData.tab, sectionInstructions);
1632
- }
1633
- if (formData.order !== undefined) {
1634
- destForm.splice(formData.order, 0, sectionInstructions);
1635
- }
1636
- else {
1637
- destForm.push(sectionInstructions);
1638
- }
1933
+ if (source.hasOwnProperty(field)) {
1934
+ if (field === '_id') {
1935
+ if (destList && source['_id'].options && source['_id'].options.list) {
1936
+ handleListInfo(destList, source['_id'].options.list, field);
1639
1937
  }
1640
1938
  }
1641
- else {
1642
- if (destForm && !formData.hidden) {
1643
- var formInstructions = basicInstructions(field, formData, prefix);
1644
- if (handleConditionals(formInstructions.showIf, formInstructions.name, $scope) && field !== 'options') {
1645
- var formInst = handleFieldType(formInstructions, mongooseType, mongooseOptions, $scope, ctrlState);
1646
- if (formInst.tab) {
1647
- handletabInfo(formInst.tab, formInst);
1939
+ else if (source.hasOwnProperty(field)) {
1940
+ var mongooseType = source[field], mongooseOptions = mongooseType.options || {};
1941
+ var formData = mongooseOptions.form || {};
1942
+ if (mongooseType.schema && !formData.hidden) {
1943
+ if (doRecursion && destForm) {
1944
+ var schemaSchema = [];
1945
+ handleSchema('Nested ' + field, mongooseType.schema, schemaSchema, null, field + '.', true, $scope, ctrlState);
1946
+ var sectionInstructions = basicInstructions(field, formData, prefix);
1947
+ sectionInstructions.schema = schemaSchema;
1948
+ if (formData.tab) {
1949
+ handletabInfo(formData.tab, sectionInstructions);
1648
1950
  }
1649
1951
  if (formData.order !== undefined) {
1650
- destForm.splice(formData.order, 0, formInst);
1952
+ destForm.splice(formData.order, 0, sectionInstructions);
1651
1953
  }
1652
1954
  else {
1653
- destForm.push(formInst);
1955
+ destForm.push(sectionInstructions);
1654
1956
  }
1655
1957
  }
1656
1958
  }
1657
- if (destList && mongooseOptions.list) {
1658
- handleListInfo(destList, mongooseOptions.list, field);
1959
+ else {
1960
+ if (destForm && !formData.hidden) {
1961
+ var formInstructions = basicInstructions(field, formData, prefix);
1962
+ if (handleConditionals(formInstructions.showIf, formInstructions.name, $scope) && field !== 'options') {
1963
+ var formInst = handleFieldType(formInstructions, mongooseType, mongooseOptions, $scope, ctrlState);
1964
+ if (formInst.tab) {
1965
+ handletabInfo(formInst.tab, formInst);
1966
+ }
1967
+ if (formData.order !== undefined) {
1968
+ destForm.splice(formData.order, 0, formInst);
1969
+ }
1970
+ else {
1971
+ destForm.push(formInst);
1972
+ }
1973
+ }
1974
+ }
1975
+ if (destList && mongooseOptions.list) {
1976
+ handleListInfo(destList, mongooseOptions.list, field);
1977
+ }
1659
1978
  }
1660
1979
  }
1661
1980
  }
@@ -1679,6 +1998,9 @@ var fng;
1679
1998
  // console.log($scope.tabs[0]['title'])
1680
1999
  // $location.hash($scope.tabs[0]['title']);
1681
2000
  // }
2001
+ if (typeof $scope.onSchemaProcessed === "function") {
2002
+ $scope.onSchemaProcessed(description, description.slice(0, 5) === 'Main ' ? $scope.baseSchema() : destForm);
2003
+ }
1682
2004
  if (destList && destList.length === 0) {
1683
2005
  handleEmptyList(description, destList, destForm, source);
1684
2006
  }
@@ -1687,23 +2009,23 @@ var fng;
1687
2009
  function performLookupSelect() {
1688
2010
  formInstructions.options = recordHandler.suffixCleanId(formInstructions, 'Options');
1689
2011
  formInstructions.ids = recordHandler.suffixCleanId(formInstructions, '_ids');
1690
- if (!formInstructions.hidden && mongooseOptions.ref) {
1691
- if (typeof mongooseOptions.ref === 'string') {
1692
- mongooseOptions.ref = { type: 'lookup', collection: mongooseOptions.ref };
1693
- console.warn("Support for string type \"ref\" property is deprecated - use ref:" + JSON.stringify(mongooseOptions.ref));
1694
- }
1695
- switch (mongooseOptions.ref.type) {
1696
- case 'lookup':
1697
- recordHandler.setUpLookupOptions(mongooseOptions.ref.collection, formInstructions, $scope, ctrlState, handleSchema);
1698
- break;
1699
- case 'lookupList':
1700
- recordHandler.setUpLookupListOptions(mongooseOptions.ref, formInstructions, $scope, ctrlState);
1701
- break;
1702
- case 'internal':
1703
- recordHandler.handleInternalLookup($scope, formInstructions, mongooseOptions.ref);
1704
- break;
1705
- default:
1706
- throw new Error("Unsupported ref type " + mongooseOptions.ref.type + " found in " + formInstructions.name);
2012
+ if (!formInstructions.hidden) {
2013
+ if (mongooseOptions.ref) {
2014
+ recordHandler.setUpLookupOptions(mongooseOptions.ref, formInstructions, $scope, ctrlState, handleSchema);
2015
+ }
2016
+ else if (mongooseOptions.lookupListRef) {
2017
+ recordHandler.setUpLookupListOptions(mongooseOptions.lookupListRef, formInstructions, $scope, ctrlState);
2018
+ formInstructions.lookupListRef = mongooseOptions.lookupListRef;
2019
+ }
2020
+ else if (mongooseOptions.internalRef) {
2021
+ recordHandler.handleInternalLookup($scope, formInstructions, mongooseOptions.internalRef);
2022
+ formInstructions.internalRef = mongooseOptions.internalRef;
2023
+ }
2024
+ else if (mongooseOptions.customLookupOptions) {
2025
+ // nothing to do - call setUpCustomLookupOptions() when ready to provide id and option arrays
2026
+ }
2027
+ else {
2028
+ throw new Error("No supported select lookup type found in ".concat(formInstructions.name));
1707
2029
  }
1708
2030
  }
1709
2031
  }
@@ -1715,7 +2037,7 @@ var fng;
1715
2037
  angular.extend(formInstructions, mongooseType.options.form);
1716
2038
  }
1717
2039
  }
1718
- if (mongooseType.instance === 'String') {
2040
+ if (mongooseType.instance === 'String' || (mongooseType.instance === 'ObjectID' && formInstructions.asText)) {
1719
2041
  if (mongooseOptions.enum) {
1720
2042
  formInstructions.type = formInstructions.type || 'select';
1721
2043
  if (formInstructions.select2) {
@@ -1737,13 +2059,22 @@ var fng;
1737
2059
  }
1738
2060
  else if (mongooseType.instance === 'ObjectID') {
1739
2061
  formInstructions.ref = mongooseOptions.ref;
1740
- if (formInstructions.link && formInstructions.link.linkOnly) {
1741
- formInstructions.type = 'link';
1742
- formInstructions.linkText = formInstructions.link.text;
2062
+ if (formInstructions.link) {
2063
+ if (formInstructions.link.linkOnly) {
2064
+ formInstructions.type = 'link';
2065
+ formInstructions.linktext = formInstructions.link.text;
2066
+ }
2067
+ else if (formInstructions.link.label) {
2068
+ formInstructions.linklabel = true;
2069
+ }
2070
+ else {
2071
+ console.log('Unsupported link setup');
2072
+ }
1743
2073
  formInstructions.form = formInstructions.link.form;
2074
+ formInstructions.linktab = formInstructions.link.linktab;
1744
2075
  delete formInstructions.link;
1745
2076
  }
1746
- else {
2077
+ if (formInstructions.type !== 'link') {
1747
2078
  formInstructions.type = 'select';
1748
2079
  if (formInstructions.select2 || (mongooseOptions.form && mongooseOptions.form.select2)) {
1749
2080
  console.log('support for fng-select2 has been removed in 0.8.3 - please convert to fng-ui-select');
@@ -1755,16 +2086,18 @@ var fng;
1755
2086
  }
1756
2087
  else if (mongooseType.instance === 'Date') {
1757
2088
  if (!formInstructions.type) {
2089
+ formInstructions.intType = 'date';
1758
2090
  if (formInstructions.readonly) {
1759
2091
  formInstructions.type = 'text';
1760
2092
  }
1761
2093
  else if (formInstructions.directive) {
1762
- formInstructions.type = 'text'; // Think they all use date
2094
+ formInstructions.type = 'text';
1763
2095
  }
1764
2096
  else {
1765
2097
  try {
1766
2098
  formInstructions.add = formInstructions.add || '';
1767
- var testDatePickerInstalled = angular.module('ui.date').requires;
2099
+ // Check whether DatePicker is installed
2100
+ angular.module('ui.date').requires;
1768
2101
  formInstructions.type = 'text';
1769
2102
  formInstructions.add += ' ui-date ui-date-format ';
1770
2103
  // formInstructions.add += ' ui-date ui-date-format datepicker-popup-fix ';
@@ -1777,10 +2110,10 @@ var fng;
1777
2110
  }
1778
2111
  }
1779
2112
  else if (mongooseType.instance.toLowerCase() === 'boolean') {
1780
- formInstructions.type = 'checkbox';
2113
+ formInstructions.type = formInstructions.type || 'checkbox';
1781
2114
  }
1782
2115
  else if (mongooseType.instance === 'Number') {
1783
- formInstructions.type = 'number';
2116
+ formInstructions.type = formInstructions.type || 'number';
1784
2117
  if (mongooseOptions.min !== undefined) {
1785
2118
  formInstructions.add = 'min="' + mongooseOptions.min + '" ' + (formInstructions.add || '');
1786
2119
  }
@@ -1800,11 +2133,17 @@ var fng;
1800
2133
  if (mongooseOptions.readonly) {
1801
2134
  formInstructions['readonly'] = true;
1802
2135
  }
2136
+ if (mongooseType.defaultValue !== undefined) {
2137
+ formInstructions.defaultValue = mongooseType.defaultValue;
2138
+ }
2139
+ else if (mongooseType.options && mongooseType.options.default !== undefined) {
2140
+ console.log('No support for default with no value, yet');
2141
+ }
1803
2142
  return formInstructions;
1804
2143
  }
1805
- function getArrayFieldToExtend(fieldName, $scope) {
2144
+ function getArrayFieldToExtend(fieldName, $scope, modelOverride) {
1806
2145
  var fieldParts = fieldName.split('.');
1807
- var arrayField = $scope.record;
2146
+ var arrayField = modelOverride || $scope.record;
1808
2147
  for (var i = 0, l = fieldParts.length; i < l; i++) {
1809
2148
  if (!arrayField[fieldParts[i]]) {
1810
2149
  if (i === l - 1) {
@@ -1921,6 +2260,9 @@ var fng;
1921
2260
  generateEditUrl: function generateEditUrl(obj, $scope) {
1922
2261
  return routingService.buildUrl($scope.modelName + '/' + ($scope.formName ? $scope.formName + '/' : '') + obj._id + '/edit');
1923
2262
  },
2263
+ generateViewUrl: function generateViewUrl(obj, $scope) {
2264
+ return routingService.buildUrl($scope.modelName + '/' + ($scope.formName ? $scope.formName + '/' : '') + obj._id + '/view');
2265
+ },
1924
2266
  generateNewUrl: function generateNewUrl($scope) {
1925
2267
  return routingService.buildUrl($scope.modelName + '/' + ($scope.formName ? $scope.formName + '/' : '') + 'new');
1926
2268
  },
@@ -2005,25 +2347,55 @@ var fng;
2005
2347
  }
2006
2348
  return forceNextTime;
2007
2349
  },
2008
- add: function add(fieldName, $event, $scope) {
2009
- var arrayField = getArrayFieldToExtend(fieldName, $scope);
2010
- arrayField.push({});
2011
- $scope.setFormDirty($event);
2350
+ add: function add(fieldName, $event, $scope, modelOverride) {
2351
+ var _a, _b;
2352
+ // for buttons, the click event won't fire if the disabled attribute exists, but the same is not true of
2353
+ // icons, so we need to check this for simple array item addition
2354
+ if (((_a = $event === null || $event === void 0 ? void 0 : $event.target) === null || _a === void 0 ? void 0 : _a.hasAttribute) && $event.target.hasAttribute("disabled")) {
2355
+ return $event.preventDefault();
2356
+ }
2357
+ // check that target element is visible. May not be reliable - see https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
2358
+ if ($event.target.offsetParent) {
2359
+ var arrayField = getArrayFieldToExtend(fieldName, $scope, modelOverride);
2360
+ var schemaElement = $scope.formSchema.find(function (f) { return f.name === fieldName; }); // In case someone is using the formSchema directly
2361
+ var subSchema = schemaElement ? schemaElement.schema : null;
2362
+ var obj = subSchema ? $scope.setDefaults(subSchema, fieldName + '.') : {};
2363
+ if (typeof ((_b = $scope.dataEventFunctions) === null || _b === void 0 ? void 0 : _b.onInitialiseNewSubDoc) === "function") {
2364
+ $scope.dataEventFunctions.onInitialiseNewSubDoc(fieldName, subSchema, obj);
2365
+ }
2366
+ arrayField.push(obj);
2367
+ $scope.setFormDirty($event);
2368
+ }
2012
2369
  },
2013
- unshift: function unshift(fieldName, $event, $scope) {
2014
- var arrayField = getArrayFieldToExtend(fieldName, $scope);
2370
+ unshift: function unshift(fieldName, $event, $scope, modelOverride) {
2371
+ var arrayField = getArrayFieldToExtend(fieldName, $scope, modelOverride);
2015
2372
  arrayField.unshift({});
2016
2373
  $scope.setFormDirty($event);
2017
2374
  },
2018
- remove: function remove(fieldName, value, $event, $scope) {
2375
+ remove: function remove(fieldName, value, $event, $scope, modelOverride) {
2376
+ var _a;
2377
+ // for buttons, the click event won't fire if the disabled attribute exists, but the same is not true of
2378
+ // icons, so we need to check this for simple array item removal
2379
+ if (((_a = $event === null || $event === void 0 ? void 0 : $event.target) === null || _a === void 0 ? void 0 : _a.hasAttribute) && $event.target.hasAttribute("disabled")) {
2380
+ return $event.preventDefault();
2381
+ }
2019
2382
  // Remove an element from an array
2020
- var fieldParts = fieldName.split('.');
2021
- var arrayField = $scope.record;
2022
- for (var i = 0, l = fieldParts.length; i < l; i++) {
2023
- arrayField = arrayField[fieldParts[i]];
2383
+ var arrayField = getArrayFieldToExtend(fieldName, $scope, modelOverride);
2384
+ var err;
2385
+ if (typeof $scope.dataEventFunctions.onDeleteSubDoc === "function") {
2386
+ var schemaElement = $scope.formSchema.find(function (f) {
2387
+ return f.name === fieldName;
2388
+ });
2389
+ var subSchema = schemaElement ? schemaElement.schema : null;
2390
+ err = $scope.dataEventFunctions.onDeleteSubDoc(fieldName, subSchema, arrayField, value);
2391
+ }
2392
+ if (err) {
2393
+ $scope.showError(err);
2394
+ }
2395
+ else {
2396
+ arrayField.splice(value, 1);
2397
+ $scope.setFormDirty($event);
2024
2398
  }
2025
- arrayField.splice(value, 1);
2026
- $scope.setFormDirty($event);
2027
2399
  },
2028
2400
  hasError: function hasError(formName, name, index, $scope) {
2029
2401
  var result = false;
@@ -2035,7 +2407,7 @@ var fng;
2035
2407
  // Cannot assume that directives will use the same methods
2036
2408
  if (form) {
2037
2409
  var field_1 = form[name];
2038
- if (field_1 && field_1.$invalid) {
2410
+ if (field_1 && field_1.$invalid && !field_1.$$attr.readonly) {
2039
2411
  if (field_1.$dirty) {
2040
2412
  result = true;
2041
2413
  }
@@ -2055,7 +2427,7 @@ var fng;
2055
2427
  }
2056
2428
  return result;
2057
2429
  },
2058
- decorateScope: function decorateScope($scope, formGeneratorInstance, recordHandlerInstance, sharedData) {
2430
+ decorateScope: function decorateScope($scope, formGeneratorInstance, recordHandlerInstance, sharedData, pseudoUrl) {
2059
2431
  $scope.record = sharedData.record;
2060
2432
  $scope.phase = 'init';
2061
2433
  $scope.disableFunctions = sharedData.disableFunctions;
@@ -2072,17 +2444,34 @@ var fng;
2072
2444
  $scope.pageSize = 60;
2073
2445
  $scope.pagesLoaded = 0;
2074
2446
  sharedData.baseScope = $scope;
2447
+ securityService.decorateSecurableScope($scope, { pseudoUrl: pseudoUrl });
2075
2448
  $scope.generateEditUrl = function (obj) {
2076
2449
  return formGeneratorInstance.generateEditUrl(obj, $scope);
2077
2450
  };
2451
+ $scope.generateViewUrl = function (obj) {
2452
+ return formGeneratorInstance.generateViewUrl(obj, $scope);
2453
+ };
2078
2454
  $scope.generateNewUrl = function () {
2079
2455
  return formGeneratorInstance.generateNewUrl($scope);
2080
2456
  };
2081
2457
  $scope.scrollTheList = function () {
2082
- return recordHandlerInstance.scrollTheList($scope);
2458
+ var _a;
2459
+ // wait until we have the list schema. until we get a non-empty listSchema (which might never
2460
+ // happen if we don't have permission to GET it), then there's no point requesting the data
2461
+ if (((_a = $scope.listSchema) === null || _a === void 0 ? void 0 : _a.length) > 0) {
2462
+ return recordHandlerInstance.scrollTheList($scope);
2463
+ }
2464
+ else {
2465
+ var unwatch_1 = $scope.$watchCollection("listSchema", function (newValue) {
2466
+ if ((newValue === null || newValue === void 0 ? void 0 : newValue.length) > 0) {
2467
+ unwatch_1();
2468
+ return recordHandlerInstance.scrollTheList($scope);
2469
+ }
2470
+ });
2471
+ }
2083
2472
  };
2084
2473
  $scope.getListData = function (record, fieldName) {
2085
- return recordHandlerInstance.getListData($scope, record, fieldName, $scope.listSchema);
2474
+ return recordHandlerInstance.getListData(record, fieldName, $scope.listSchema, $scope);
2086
2475
  };
2087
2476
  $scope.setPristine = function (clearErrors) {
2088
2477
  if (clearErrors) {
@@ -2104,17 +2493,17 @@ var fng;
2104
2493
  console.log('setFormDirty called without an event (fine in a unit test)');
2105
2494
  }
2106
2495
  };
2107
- $scope.add = function (fieldName, $event) {
2108
- return formGeneratorInstance.add(fieldName, $event, $scope);
2496
+ $scope.add = function (fieldName, $event, modelOverride) {
2497
+ return formGeneratorInstance.add(fieldName, $event, $scope, modelOverride);
2109
2498
  };
2110
2499
  $scope.hasError = function (form, name, index) {
2111
2500
  return formGeneratorInstance.hasError(form, name, index, $scope);
2112
2501
  };
2113
- $scope.unshift = function (fieldName, $event) {
2114
- return formGeneratorInstance.unshift(fieldName, $event, $scope);
2502
+ $scope.unshift = function (fieldName, $event, modelOverride) {
2503
+ return formGeneratorInstance.unshift(fieldName, $event, $scope, modelOverride);
2115
2504
  };
2116
- $scope.remove = function (fieldName, value, $event) {
2117
- return formGeneratorInstance.remove(fieldName, value, $event, $scope);
2505
+ $scope.remove = function (fieldName, value, $event, modelOverride) {
2506
+ return formGeneratorInstance.remove(fieldName, value, $event, $scope, modelOverride);
2118
2507
  };
2119
2508
  $scope.baseSchema = function () {
2120
2509
  return ($scope.tabs.length ? $scope.tabs : $scope.formSchema);
@@ -2129,7 +2518,7 @@ var fng;
2129
2518
  };
2130
2519
  }
2131
2520
  services.formGenerator = formGenerator;
2132
- formGenerator.$inject = ["$location", "$timeout", "$filter", "SubmissionsService", "routingService", "recordHandler"];
2521
+ formGenerator.$inject = ["$filter", "routingService", "recordHandler", "securityService"];
2133
2522
  })(services = fng.services || (fng.services = {}));
2134
2523
  })(fng || (fng = {}));
2135
2524
  /// <reference path="../../index.d.ts" />
@@ -2138,8 +2527,8 @@ var fng;
2138
2527
  var services;
2139
2528
  (function (services) {
2140
2529
  /*@ngInject*/
2141
- formMarkupHelper.$inject = ["cssFrameworkService", "inputSizeHelper", "addAllService"];
2142
- function formMarkupHelper(cssFrameworkService, inputSizeHelper, addAllService) {
2530
+ formMarkupHelper.$inject = ["cssFrameworkService", "inputSizeHelper", "addAllService", "securityService", "$filter"];
2531
+ function formMarkupHelper(cssFrameworkService, inputSizeHelper, addAllService, securityService, $filter) {
2143
2532
  function generateNgShow(showWhen, model) {
2144
2533
  function evaluateSide(side) {
2145
2534
  var result = side;
@@ -2167,29 +2556,190 @@ var fng;
2167
2556
  }
2168
2557
  return evaluateSide(showWhen.lhs) + conditionSymbols[conditionPos] + evaluateSide(showWhen.rhs);
2169
2558
  }
2170
- var isHorizontalStyle = function isHorizontalStyle(formStyle) {
2171
- return (!formStyle || formStyle === 'undefined' || ['vertical', 'inline'].indexOf(formStyle) === -1);
2559
+ var isHorizontalStyle = function isHorizontalStyle(formStyle, includeStacked) {
2560
+ var exclude = ['vertical', 'inline'];
2561
+ if (!includeStacked) {
2562
+ exclude.push('stacked');
2563
+ }
2564
+ return (!formStyle || formStyle === 'undefined' || !exclude.includes(formStyle));
2172
2565
  };
2173
2566
  function glyphClass() {
2174
- return (cssFrameworkService.framework() === 'bs2') ? 'icon' : 'glyphicon glyphicon';
2567
+ return (cssFrameworkService.framework() === 'bs2' ? 'icon' : 'glyphicon glyphicon');
2568
+ }
2569
+ // Generate two strings:
2570
+ // 1. firstly, attribute(s) that could be added to element(s) representing the field with the given
2571
+ // parameters to enable or disable it according to the prevailing security rules.
2572
+ // 2. secondly, attribute(s) that could be added to a element - regardless of whether or not it will
2573
+ // actually be disabled on this occasion - to identify it as being potentially disableable
2574
+ // This function is a more complicated version of securityService.generateDisabledAttr, also taking into
2575
+ // account the fact that fieldInfo.readonly can influence the disabled state of a field.
2576
+ // nonUniqueId should be required only in cases where a sub-sub schema has been defined in a directive
2577
+ // as a means of getting around the single-level-of-nesting limitation. in that case, where the
2578
+ // directive's template then includes a <form-input> tag, it is likely that the ids of the sub-sub-schema
2579
+ // elements will include $index from a parent scope (responsible for the sub-schema) in order to
2580
+ // ensure its uniqueness, and in this case (as we are not explicitely managing the addition of the
2581
+ // {{ $index }} expr), we need to be given a version of the id that does not include that expression.
2582
+ // where nonUniqueId is provided, we will also use this for determining ancestors, because in the
2583
+ // nested sub-schema scenario described above, the names of the fields in the sub-sub-schema will
2584
+ // probably not identify the full ancestry.
2585
+ function handleReadOnlyDisabled(partialFieldInfo, scope) {
2586
+ var id = partialFieldInfo.nonUniqueId || partialFieldInfo.id;
2587
+ function getActuallyDisabledAttr() {
2588
+ if (partialFieldInfo.readonly && typeof partialFieldInfo.readonly === "boolean") {
2589
+ // if we have a true-valued readonly property then this trumps whatever security rule might apply to this field
2590
+ return " disabled ";
2591
+ }
2592
+ function wrapReadOnly() {
2593
+ return partialFieldInfo.readonly ? " ng-disabled=\"".concat(partialFieldInfo.readonly, "\" ") : "";
2594
+ }
2595
+ if (!id || !securityService.canDoSecurityNow(scope, "disabled")) {
2596
+ // no security, so we're just concerned about what value fieldInfo.readonly has
2597
+ return wrapReadOnly();
2598
+ }
2599
+ // if scope has been decorated with a requiresDisabledChildren function, we will be using that to check whether any
2600
+ // of the ancestors of this field's element require their children to be disabled. if they do, that means us!
2601
+ var ancestorIds = [];
2602
+ if (!!scope.requiresDisabledChildren) {
2603
+ var ancestors = void 0;
2604
+ // if we have been provided with a nonUniqueId, we should use that to determine ancestors, because in this case,
2605
+ // the name will not be reliable
2606
+ if (partialFieldInfo.nonUniqueId) {
2607
+ var ancestorStr = partialFieldInfo.nonUniqueId.startsWith("f_") ? partialFieldInfo.nonUniqueId.substring(2) : partialFieldInfo.nonUniqueId;
2608
+ ancestors = ancestorStr.split("_");
2609
+ }
2610
+ else {
2611
+ ancestors = partialFieldInfo.name.split(".");
2612
+ }
2613
+ ancestors.pop();
2614
+ while (ancestors.length > 0) {
2615
+ ancestorIds.push("f_".concat(ancestors.join("_")));
2616
+ ancestors.pop();
2617
+ }
2618
+ }
2619
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
2620
+ // "instant" security is evaluated now, and a positive result trumps whatever fieldInfo.readonly might be set to
2621
+ if (scope.isSecurelyDisabled(id)) {
2622
+ return " disabled ";
2623
+ }
2624
+ else {
2625
+ for (var _i = 0, ancestorIds_1 = ancestorIds; _i < ancestorIds_1.length; _i++) {
2626
+ var ancestorId = ancestorIds_1[_i];
2627
+ if (scope.requiresDisabledChildren(ancestorId)) {
2628
+ return " disabled ";
2629
+ }
2630
+ }
2631
+ return wrapReadOnly();
2632
+ }
2633
+ }
2634
+ var securityFuncStr = "isSecurelyDisabled('".concat(id, "')");
2635
+ if (ancestorIds.length > 0) {
2636
+ var ancestorStr = ancestorIds.map(function (aid) { return "requiresDisabledChildren('".concat(aid, "')"); });
2637
+ securityFuncStr = "(".concat(securityFuncStr, " || ").concat(ancestorStr.join(" || "), ")");
2638
+ }
2639
+ var oneTimeBinding = fng.formsAngular.elemSecurityFuncBinding === "one-time";
2640
+ if (partialFieldInfo.readonly) {
2641
+ // we have both security and a read-only attribute to deal with
2642
+ if (oneTimeBinding) {
2643
+ // if our field has a string-typed readonly attribute *and* one-time binding is required by our securityFunc, we
2644
+ // cannot simply combine these into a single ng-disabled expression, because the readonly property is highly
2645
+ // likely to be model-dependent and therefore cannot use one-time-binding. the best we can do in this case is
2646
+ // to use ng-disabled for the field's readonly property, and a one-time-bound ng-readonly for the securityFunc.
2647
+ // this is not perfect, because in the case of selects, ng-readonly doesn't actually prevent the user from
2648
+ // making a selection. however, the select will be styled as if it is disabled (including the not-allowed
2649
+ // cursor), which should deter the user in most cases.
2650
+ return wrapReadOnly() + "ng-readonly=\"::".concat(securityFuncStr, "\" ");
2651
+ }
2652
+ else {
2653
+ // if we have both things and we are *NOT* required to use one-time binding for the securityFunc, then they can
2654
+ // be combined into a single ng-disabled expression
2655
+ return " ng-disabled=\"".concat(securityFuncStr, " || ").concat(partialFieldInfo.readonly, "\" ");
2656
+ }
2657
+ }
2658
+ else {
2659
+ // we have security only
2660
+ return " ng-disabled=\"".concat(oneTimeBinding ? "::" : "").concat(securityFuncStr, "\" ");
2661
+ }
2662
+ }
2663
+ return [getActuallyDisabledAttr(), securityService.getDisableableAttrs(id)];
2664
+ }
2665
+ function generateArrayElementIdString(idString, info, options) {
2666
+ if (options.subschema && options.model) {
2667
+ // for subschemas, it is possible that our model will begin with $parent., or $parent.$parent. (etc). though a bit of
2668
+ // a hack where this does occur (probably where a directive used by a sub-schema is using a nested <form-input>
2669
+ // directive), we need to look for the $index in the same place as our model is looking for data.
2670
+ var model = options.model;
2671
+ var nestedSteps = 0;
2672
+ var stepIndicator = "$parent.";
2673
+ while (model.startsWith(stepIndicator)) {
2674
+ nestedSteps++;
2675
+ model = model.substring(stepIndicator.length);
2676
+ }
2677
+ return "".concat(idString, "_{{").concat(stepIndicator.repeat(nestedSteps), "$index}}");
2678
+ }
2679
+ else {
2680
+ return "".concat(idString, "_{{$index}}");
2681
+ }
2682
+ }
2683
+ function genDisableableAncestorStr(id) {
2684
+ return securityService.getDisableableAncestorAttrs(id);
2685
+ }
2686
+ function isArrayElement(scope, info, options) {
2687
+ return scope["$index"] !== undefined || !!options.subschema;
2175
2688
  }
2176
2689
  return {
2177
2690
  isHorizontalStyle: isHorizontalStyle,
2691
+ isArrayElement: isArrayElement,
2178
2692
  fieldChrome: function fieldChrome(scope, info, options) {
2693
+ var insert = '';
2694
+ if (info.id && typeof info.id.replace === "function") {
2695
+ var uniqueIdStr = info.nonUniqueId || info.nonuniqueid || info.id;
2696
+ var idStr = void 0;
2697
+ // replace any . that appear in info.id with "-", but not those that appear between {{ and }}
2698
+ if (info.id.includes(".") && info.id.includes("{{")) {
2699
+ idStr = "cg_";
2700
+ var inExpr = false;
2701
+ for (var i = 0; i < info.id.length; i++) {
2702
+ if (info.id[i] === "{" && info.id[i - 1] === "{") {
2703
+ inExpr = true;
2704
+ }
2705
+ else if (info.id[i] === "}" && info.id[i - 1] === "}") {
2706
+ inExpr = false;
2707
+ }
2708
+ if (inExpr || info.id[i] !== ".") {
2709
+ idStr += info.id[i];
2710
+ }
2711
+ else {
2712
+ idStr += "-";
2713
+ }
2714
+ }
2715
+ }
2716
+ else {
2717
+ idStr = "cg_".concat(info.id.replace(/\./g, '-'));
2718
+ }
2719
+ uniqueIdStr = "cg_".concat(uniqueIdStr.replace(/\./g, '-'));
2720
+ var visibility = securityService.considerVisibility(uniqueIdStr, scope);
2721
+ if (visibility.omit) {
2722
+ // we already know this field should be invisible, so we needn't add anything for it
2723
+ return { omit: true };
2724
+ }
2725
+ insert += "id=\"".concat(isArrayElement(scope, info, options) ? generateArrayElementIdString(idStr, info, options) : idStr, "\"");
2726
+ if (visibility.visibilityAttr) {
2727
+ // an angular expression to determine the visibility of this field later...
2728
+ insert += " ".concat(visibility.visibilityAttr);
2729
+ }
2730
+ }
2179
2731
  var classes = info.classes || '';
2180
2732
  var template = '';
2181
2733
  var closeTag = '';
2182
- var insert = '';
2183
2734
  info.showWhen = info.showWhen || info.showwhen; // deal with use within a directive
2184
2735
  if (info.showWhen) {
2185
2736
  if (typeof info.showWhen === 'string') {
2186
- insert += 'ng-show="' + info.showWhen + '"';
2737
+ insert += ' ng-show="' + info.showWhen + '"';
2187
2738
  }
2188
2739
  else {
2189
- insert += 'ng-show="' + generateNgShow(info.showWhen, options.model) + '"';
2740
+ insert += ' ng-show="' + generateNgShow(info.showWhen, options.model) + '"';
2190
2741
  }
2191
2742
  }
2192
- insert += ' id="cg_' + info.id.replace(/\./g, '-') + '"';
2193
2743
  if (cssFrameworkService.framework() === 'bs3') {
2194
2744
  classes += ' form-group';
2195
2745
  if (options.formstyle === 'vertical' && info.size !== 'block-level') {
@@ -2214,7 +2764,7 @@ var fng;
2214
2764
  closeTag += '</div>';
2215
2765
  }
2216
2766
  else {
2217
- if (isHorizontalStyle(options.formstyle)) {
2767
+ if (isHorizontalStyle(options.formstyle, true)) {
2218
2768
  template += '<div' + addAllService.addAll(scope, 'Group', 'control-group', options);
2219
2769
  closeTag = '</div>';
2220
2770
  }
@@ -2228,11 +2778,13 @@ var fng;
2228
2778
  },
2229
2779
  label: function label(scope, fieldInfo, addButtonMarkup, options) {
2230
2780
  var labelHTML = '';
2231
- if ((cssFrameworkService.framework() === 'bs3' || (options.formstyle !== 'inline' && fieldInfo.label !== '')) || addButtonMarkup) {
2781
+ if ((cssFrameworkService.framework() === 'bs3' || (!['inline', 'stacked'].includes(options.formstyle) && fieldInfo.label !== '')) || addButtonMarkup) {
2232
2782
  labelHTML = '<label';
2233
2783
  var classes = 'control-label';
2234
- if (isHorizontalStyle(options.formstyle)) {
2235
- labelHTML += ' for="' + fieldInfo.id + '"';
2784
+ if (isHorizontalStyle(options.formstyle, false)) {
2785
+ if (!fieldInfo.linklabel) {
2786
+ labelHTML += ' for="' + fieldInfo.id + '"';
2787
+ }
2236
2788
  if (typeof fieldInfo.labelDefaultClass !== 'undefined') {
2237
2789
  // Override default label class (can be empty)
2238
2790
  classes += ' ' + fieldInfo.labelDefaultClass;
@@ -2241,15 +2793,27 @@ var fng;
2241
2793
  classes += ' col-sm-3';
2242
2794
  }
2243
2795
  }
2244
- else if (options.formstyle === 'inline') {
2796
+ else if (['inline', 'stacked'].includes(options.formstyle)) {
2245
2797
  labelHTML += ' for="' + fieldInfo.id + '"';
2246
2798
  classes += ' sr-only';
2247
2799
  }
2248
2800
  labelHTML += addAllService.addAll(scope, 'Label', null, options) + ' class="' + classes + '">' + fieldInfo.label;
2249
2801
  if (addButtonMarkup) {
2250
- labelHTML += ' <i id="add_' + fieldInfo.id + '" ng-click="add(\'' + fieldInfo.name + '\',$event)" class="' + glyphClass() + '-plus-sign"></i>';
2802
+ var disabledAttrs = handleReadOnlyDisabled(fieldInfo, scope);
2803
+ labelHTML += " <i ".concat(disabledAttrs.join(" "), " id=\"add_").concat(fieldInfo.id, "\" ng-click=\"add('").concat(fieldInfo.name, "', $event)\" class=\"").concat(glyphClass(), "-plus-sign\"></i>");
2251
2804
  }
2252
2805
  labelHTML += '</label>';
2806
+ if (fieldInfo.linklabel) {
2807
+ var value = '<fng-link fld="' + fieldInfo.name + '" ref="' + fieldInfo.ref + '" text="' + escape(labelHTML) + '"';
2808
+ if (fieldInfo.form) {
2809
+ value += ' form="' + fieldInfo.form + '"';
2810
+ }
2811
+ if (fieldInfo.linktab) {
2812
+ value += ' linktab="' + fieldInfo.linktab + '"';
2813
+ }
2814
+ value += '></fng-link>';
2815
+ labelHTML = value;
2816
+ }
2253
2817
  }
2254
2818
  return labelHTML;
2255
2819
  },
@@ -2269,13 +2833,27 @@ var fng;
2269
2833
  else {
2270
2834
  sizeClassBS2 = (fieldInfo.size ? ' input-' + fieldInfo.size : '');
2271
2835
  }
2272
- if (options.formstyle === 'inline') {
2836
+ if (['inline', 'stacked'].includes(options.formstyle)) {
2273
2837
  placeHolder = placeHolder || fieldInfo.label;
2274
2838
  }
2275
- common = 'ng-model="' + modelString + '"' + (idString ? ' id="' + idString + '" name="' + idString + '" ' : ' name="' + nameString + '" ');
2276
- common += (placeHolder ? ('placeholder="' + placeHolder + '" ') : '');
2839
+ common = 'data-ng-model="' + modelString + '"';
2840
+ if (idString) {
2841
+ common += " id=\"".concat(idString, "\"");
2842
+ }
2843
+ if (nameString) {
2844
+ common += " name=\"".concat(nameString, "\"");
2845
+ }
2846
+ else if (idString) {
2847
+ common += " name=\"".concat(idString, "\"");
2848
+ }
2849
+ if (placeHolder) {
2850
+ common += " placeholder=\"".concat(placeHolder, "\"");
2851
+ }
2277
2852
  if (fieldInfo.popup) {
2278
- common += 'title="' + fieldInfo.popup + '" ';
2853
+ common += " title=\"".concat(fieldInfo.popup, "\"");
2854
+ }
2855
+ if (fieldInfo.ariaLabel) {
2856
+ common += " aria-label=\"".concat(fieldInfo.ariaLabel, "\"");
2279
2857
  }
2280
2858
  common += addAllService.addAll(scope, 'Field', null, options);
2281
2859
  return {
@@ -2287,28 +2865,39 @@ var fng;
2287
2865
  };
2288
2866
  },
2289
2867
  inputChrome: function inputChrome(value, fieldInfo, options, markupVars) {
2290
- if (cssFrameworkService.framework() === 'bs3' && isHorizontalStyle(options.formstyle) && fieldInfo.type !== 'checkbox') {
2868
+ if (cssFrameworkService.framework() === 'bs3' && isHorizontalStyle(options.formstyle, true) && fieldInfo.type !== 'checkbox') {
2291
2869
  value = '<div class="bs3-input ' + markupVars.sizeClassBS3 + '">' + value + '</div>';
2292
2870
  }
2293
2871
  // Hack to cope with inline help in directives
2294
2872
  var inlineHelp = (fieldInfo.helpInline || '') + (fieldInfo.helpinline || '');
2295
2873
  if (inlineHelp.length > 0) {
2296
- value += '<span class="' + (cssFrameworkService.framework() === 'bs2' ? 'help-inline' : 'help-block') + '">' + inlineHelp + '</span>';
2297
- }
2298
- // If we have chosen
2299
- value += '<div ng-if="' + (options.name || 'myForm') + '.' + fieldInfo.id + '.$dirty" class="help-block">' +
2300
- ' <div ng-messages="' + (options.name || 'myForm') + '.' + fieldInfo.id + '.$error">' +
2301
- ' <div ng-messages-include="error-messages.html">' +
2302
- ' </div>' +
2303
- ' </div>' +
2304
- '</div>';
2874
+ var helpMarkup = cssFrameworkService.framework() === 'bs2' ? { el: 'span', cl: 'help-inline' } : { el: 'div', cl: 'help-block' };
2875
+ value += "<".concat(helpMarkup.el, " class=\"").concat(helpMarkup.cl, "\">").concat(inlineHelp, "</").concat(helpMarkup.el, ">");
2876
+ }
2877
+ // this is a dummy tag identifying where the input ends and the messages block (that is only visible when the form field is $dirty)
2878
+ // begins. our caller could replace this tag with anything it needs to insert between these two things.
2879
+ value += "<dms/>";
2880
+ if (!options.noid) {
2881
+ value += "<div ng-if=\"".concat((options.name || 'myForm'), "['").concat(fieldInfo.id, "'].$dirty\" class=\"help-block\">") +
2882
+ " <div ng-messages=\"".concat((options.name || 'myForm'), "['").concat(fieldInfo.id, "'].$error\">") +
2883
+ ' <div ng-messages-include="error-messages.html">' +
2884
+ ' </div>' +
2885
+ ' </div>' +
2886
+ '</div>';
2887
+ }
2305
2888
  if (fieldInfo.help) {
2306
- value += '<span class="help-block">' + fieldInfo.help + '</span>';
2889
+ value += '<div class="help-block">' + fieldInfo.help + '</div>';
2307
2890
  }
2308
2891
  return value;
2309
2892
  },
2310
2893
  generateSimpleInput: function generateSimpleInput(common, fieldInfo, options) {
2311
- var result = '<input ' + common + 'type="' + fieldInfo.type + '"';
2894
+ var result = '<input ' + common + 'type="' + fieldInfo.type + '" ';
2895
+ if (!fieldInfo.label && !fieldInfo.ariaLabel) {
2896
+ result += "aria-label=\"".concat(fieldInfo.name.replace(/\./g, ' '), "\" ");
2897
+ }
2898
+ else if (options.subschema) {
2899
+ result += "aria-label=\"".concat(fieldInfo.label ? ($filter('titleCase')(options.subschemaroot) + ' ' + fieldInfo.label) : (fieldInfo.popup || fieldInfo.name.replace(/\./g, ' ')), "\" ");
2900
+ }
2312
2901
  if (options.formstyle === 'inline' && cssFrameworkService.framework() === 'bs2' && !fieldInfo.size) {
2313
2902
  result += 'class="input-small"';
2314
2903
  }
@@ -2317,7 +2906,7 @@ var fng;
2317
2906
  },
2318
2907
  controlDivClasses: function controlDivClasses(options) {
2319
2908
  var result = [];
2320
- if (isHorizontalStyle(options.formstyle)) {
2909
+ if (isHorizontalStyle(options.formstyle, false)) {
2321
2910
  result.push(cssFrameworkService.framework() === 'bs2' ? 'controls' : 'col-sm-9');
2322
2911
  }
2323
2912
  return result;
@@ -2328,18 +2917,21 @@ var fng;
2328
2917
  }
2329
2918
  return inputMarkup;
2330
2919
  },
2331
- handleArrayInputAndControlDiv: function handleArrayInputAndControlDiv(inputMarkup, controlDivClasses, info, options) {
2332
- var result = '<div ';
2333
- if (cssFrameworkService.framework() === 'bs3') {
2334
- result += 'ng-class="skipCols($index)" ';
2335
- }
2336
- result += 'class="' + controlDivClasses.join(' ') + '" id="' + info.id + 'List" ';
2337
- result += 'ng-repeat="arrayItem in ' + (options.model || 'record') + '.' + info.name + ' track by $index">';
2338
- result += inputMarkup;
2339
- if (info.type !== 'link') {
2340
- result += '<i ng-click="remove(\'' + info.name + '\',$index,$event)" id="remove_' + info.id + '_{{$index}}" class="' + glyphClass() + '-minus-sign"></i>';
2341
- }
2920
+ handleArrayInputAndControlDiv: function handleArrayInputAndControlDiv(inputMarkup, controlDivClasses, scope, info, options) {
2921
+ var indentStr = cssFrameworkService.framework() === 'bs3' ? 'ng-class="skipCols($index)" ' : "";
2922
+ var arrayStr = (options.model || 'record') + '.' + info.name;
2923
+ var result = "";
2924
+ result += '<div id="' + info.id + 'List" class="' + controlDivClasses.join(' ') + '" ' + indentStr + ' ng-repeat="arrayItem in ' + arrayStr + ' track by $index">';
2925
+ var disabledAttrs = handleReadOnlyDisabled(info, scope);
2926
+ var removeBtn = info.type !== 'link'
2927
+ ? "<i ".concat(disabledAttrs.join(" "), " ng-click=\"remove('").concat(info.name, "', $index, $event)\" id=\"remove_").concat(info.id, "_{{$index}}\" class=\"").concat(glyphClass(), "-minus-sign\"></i>")
2928
+ : "";
2929
+ result += inputMarkup.replace("<dms/>", removeBtn);
2342
2930
  result += '</div>';
2931
+ indentStr = cssFrameworkService.framework() === 'bs3' ? 'ng-class="skipCols(' + arrayStr + '.length)" ' : "";
2932
+ if (info.help) {
2933
+ result += '<div class="array-help-block ' + controlDivClasses.join(' ') + '" ' + indentStr + ' id="empty' + info.id + 'ListHelpBlock">' + info.help + '</div>';
2934
+ }
2343
2935
  return result;
2344
2936
  },
2345
2937
  addTextInputMarkup: function addTextInputMarkup(allInputsVars, fieldInfo, requiredStr) {
@@ -2351,9 +2943,12 @@ var fng;
2351
2943
  if (fieldInfo.add) {
2352
2944
  result += ' ' + fieldInfo.add + ' ';
2353
2945
  }
2354
- result += requiredStr + (fieldInfo.readonly ? ' readonly' : '') + ' ';
2946
+ result += requiredStr;
2355
2947
  return result;
2356
- }
2948
+ },
2949
+ handleReadOnlyDisabled: handleReadOnlyDisabled,
2950
+ generateArrayElementIdString: generateArrayElementIdString,
2951
+ genDisableableAncestorStr: genDisableableAncestorStr
2357
2952
  };
2358
2953
  }
2359
2954
  services.formMarkupHelper = formMarkupHelper;
@@ -2392,103 +2987,260 @@ var fng;
2392
2987
  /*@ngInject*/
2393
2988
  pluginHelper.$inject = ["formMarkupHelper"];
2394
2989
  function pluginHelper(formMarkupHelper) {
2395
- return {
2396
- extractFromAttr: function extractFromAttr(attr, directiveName) {
2397
- function deserialize(str) {
2398
- var retVal = str.replace(/&quot;/g, '"');
2399
- if (retVal === 'true') {
2400
- retVal = true;
2990
+ function internalGenDisabledAttrs(scope, id, processedAttrs, idSuffix, params) {
2991
+ // Though id will already have the value of idSuffix appended, processedAttrs.info.name will not.
2992
+ // For handleReadOnlyDisabled() to disable "sub-elements" included in a directive template with an idsuffix when their
2993
+ // 'parent' field is disabled, we need the name to include that suffix as if it were an additional level
2994
+ // of field nesting.
2995
+ var name = processedAttrs.info.name;
2996
+ if (idSuffix) {
2997
+ if (params === null || params === void 0 ? void 0 : params.nonUniqueIdSuffix) {
2998
+ // Generally, when genIdAndDisabledStr is called from a directive, the idSuffix will be something like "select"
2999
+ // or "hasValueCheckbox" (thus enabling a single directive to create a template that includes more than one form
3000
+ // element - such as a checkbox and an input - each of which has a unique id).
3001
+ // Where a directive is responsible for creating markup for an whole array of elements, it is likely to include an
3002
+ // ng-repeat in the template that it generates, and in this case, the idSuffix that it passes to genIdAndDisabledStr
3003
+ // will probably include a reference to $index to ensure uniqueness.
3004
+ // Where idSuffix /does/ contain a reference to $index, the directive should provide a version of the idSuffix
3005
+ // in the params object which does NOT include this.
3006
+ // This is what we need to use for the ng-disabled/ng-readonly expression.
3007
+ // (ReallyCare development hint: for an example of where this is needed, see or-opts.ts.)
3008
+ id = id.replace(idSuffix, params.nonUniqueIdSuffix);
3009
+ name += ".".concat(params.nonUniqueIdSuffix);
3010
+ }
3011
+ else {
3012
+ name += ".".concat(idSuffix);
3013
+ }
3014
+ }
3015
+ var attrs = formMarkupHelper.handleReadOnlyDisabled({
3016
+ id: id,
3017
+ name: name,
3018
+ nonUniqueId: processedAttrs.info.nonuniqueid,
3019
+ readonly: processedAttrs.info.readonly
3020
+ }, scope);
3021
+ // some types of control (such as ui-select) don't deal correctly with a DISABLED attribute and
3022
+ // need ng-disabled, even when the expression is simply "true"
3023
+ if (params === null || params === void 0 ? void 0 : params.forceNg) {
3024
+ for (var i = 0; i < attrs.length; i++) {
3025
+ if (attrs[i].toLowerCase().trim() === "disabled") {
3026
+ attrs[i] = 'ng-disabled="true"';
2401
3027
  }
2402
- else if (retVal === 'false') {
2403
- retVal = false;
3028
+ }
3029
+ }
3030
+ return attrs;
3031
+ }
3032
+ function internalGenDisabledStr(scope, id, processedAttrs, idSuffix, params) {
3033
+ return internalGenDisabledAttrs(scope, id, processedAttrs, idSuffix, params).join(" ");
3034
+ }
3035
+ // text surrounded by @@ @@ is assumed to be something that can have a pseudonym. We'll rely
3036
+ // upon the relevant controller assigning a pseudo() function to baseScope.
3037
+ function handlePseudos(str) {
3038
+ if (!str) {
3039
+ return str;
3040
+ }
3041
+ var result = str;
3042
+ while (result.includes("@@")) {
3043
+ result = result.replace("@@", "{{ baseScope.pseudo('");
3044
+ result = result.replace("@@", "', true) }}");
3045
+ }
3046
+ return result;
3047
+ }
3048
+ function makeIdStringUniqueForArrayElements(scope, processedAttrs, idString) {
3049
+ if (formMarkupHelper.isArrayElement(scope, processedAttrs.info, processedAttrs.options)) {
3050
+ return formMarkupHelper.generateArrayElementIdString(idString, processedAttrs.info, processedAttrs.options);
3051
+ }
3052
+ else {
3053
+ return idString;
3054
+ }
3055
+ }
3056
+ function internalGenIdString(scope, processedAttrs, suffix, makeUniqueForArrayElements) {
3057
+ var result = processedAttrs.info.id;
3058
+ if (suffix) {
3059
+ if (!suffix.startsWith("_")) {
3060
+ result += "_";
3061
+ }
3062
+ result += suffix;
3063
+ }
3064
+ if (makeUniqueForArrayElements) {
3065
+ result = makeIdStringUniqueForArrayElements(scope, processedAttrs, result);
3066
+ }
3067
+ return result;
3068
+ }
3069
+ function internalGenDateTimePickerDisabledStr(scope, processedAttrs, idSuffix, idString) {
3070
+ var rawDisabledAttrs = internalGenDisabledAttrs(scope, idString, processedAttrs, idSuffix, { forceNg: true });
3071
+ // first, we need to convert the 'disabled' attribute(s) (those which might actually cause the element to be
3072
+ // disabled - found in rawDisabledAttrs[0]) into something that the datetime picker understands.
3073
+ var rawDisabledStr = rawDisabledAttrs[0];
3074
+ var disabledStr = "";
3075
+ // disabledStr might now include an ng-disabled attribute. To disable both the date and time inputs, we need to
3076
+ // take the value of that attribute and wrap it up as two new attributes: "disabledDate" and "readonlyTime"
3077
+ // (which is what the datetimepicker directive is expecting to receive)
3078
+ if (rawDisabledStr) {
3079
+ // disabledStr should contain either 'ng-disabled="xxxx"' or 'ng-readonly="yyyy"', or both.
3080
+ // the values of xxxx and yyyy could be more-or-less anything, and certainly they could include = or ", which
3081
+ // makes parsing hard
3082
+ // our strategy will be to re-format disabledStr as if it was the string representation of an object, and
3083
+ // then parse it. we can then refer to the ng-disabled and ng-readonly attributes of the parsed object.
3084
+ // in the future, perhaps ng-disabled and ng-readonly will be changed to data-ng-disabled and data-ng-readonly
3085
+ rawDisabledStr = rawDisabledStr.replace("data-ng-disabled", "ng-disabled");
3086
+ rawDisabledStr = rawDisabledStr.replace("data-ng-readonly", "ng-readonly");
3087
+ rawDisabledStr = rawDisabledStr.replace("ng-disabled=", '"ng-disabled":');
3088
+ rawDisabledStr = rawDisabledStr.replace("ng-readonly=", '"ng-readonly":');
3089
+ try {
3090
+ rawDisabledStr = "{ ".concat(rawDisabledStr, " }");
3091
+ var disabledObj = JSON.parse(rawDisabledStr);
3092
+ rawDisabledStr = disabledObj["ng-disabled"];
3093
+ // cannot see a way to sensibly deal with both ng-disabled and ng-readonly. Let's just ignore the ng-readonly
3094
+ // for now - with the way handleReadOnlyDisabled is currently written, this means we'll be unable to fully
3095
+ // support a datetime field with a string-typed "readonly" attribute and where fngAngular's elemSecurityFuncBinding
3096
+ // option is set up to "one-time" or "normal".
3097
+ if (rawDisabledStr) {
3098
+ disabledStr = "disabledDate=\"".concat(rawDisabledStr, "\" readonlyTime=\"").concat(rawDisabledStr, "\"");
2404
3099
  }
2405
- else if (!isNaN(parseFloat(retVal)) && isFinite(retVal)) {
2406
- retVal = parseFloat(retVal);
3100
+ }
3101
+ catch (e) {
3102
+ // give up
3103
+ }
3104
+ }
3105
+ // finally, we should add the 'disableable' attribute(s), which might be present in rawDisabledAttrs[1] (regardless
3106
+ // of whether or not the datetime picker is actually disabled) to indicate that it potentially could be
3107
+ return disabledStr + " " + rawDisabledAttrs[1];
3108
+ }
3109
+ function extractFromAttr(attr, directiveName) {
3110
+ function deserialize(str) {
3111
+ var retVal = str.replace(/&quot;/g, '"');
3112
+ if (retVal === "true") {
3113
+ return true;
3114
+ }
3115
+ else if (retVal === "false") {
3116
+ return false;
3117
+ }
3118
+ else {
3119
+ var num = parseFloat(retVal);
3120
+ if (!isNaN(num) && isFinite(num)) {
3121
+ return num;
3122
+ }
3123
+ else {
3124
+ return retVal;
2407
3125
  }
2408
- return retVal;
2409
3126
  }
2410
- var info = {};
2411
- var options = { formStyle: attr.formstyle };
2412
- var directiveOptions = {};
2413
- var directiveNameLength = directiveName ? directiveName.length : 0;
2414
- for (var prop in attr) {
2415
- if (attr.hasOwnProperty(prop)) {
2416
- if (prop.slice(0, 6) === 'fngFld') {
2417
- info[prop.slice(6).toLowerCase()] = deserialize(attr[prop]);
2418
- }
2419
- else if (prop.slice(0, 6) === 'fngOpt') {
2420
- options[prop.slice(6).toLowerCase()] = deserialize(attr[prop]);
2421
- }
2422
- else if (directiveName && prop.slice(0, directiveNameLength) === directiveName) {
2423
- directiveOptions[_.kebabCase(prop.slice(directiveNameLength))] = deserialize(attr[prop]);
2424
- }
3127
+ }
3128
+ var info = {};
3129
+ var options = { formStyle: attr.formstyle };
3130
+ var directiveOptions = {};
3131
+ var directiveNameLength = directiveName ? directiveName.length : 0;
3132
+ var lcDirectiveName = directiveName === null || directiveName === void 0 ? void 0 : directiveName.toLowerCase();
3133
+ for (var prop in attr) {
3134
+ if (attr.hasOwnProperty(prop)) {
3135
+ var lcProp = prop.toLowerCase();
3136
+ if (lcProp.slice(0, 6) === "fngfld") {
3137
+ info[lcProp.slice(6)] = deserialize(attr[prop]);
3138
+ }
3139
+ else if (lcProp.slice(0, 6) === "fngopt") {
3140
+ options[lcProp.slice(6)] = deserialize(attr[prop]);
3141
+ }
3142
+ else if (directiveName && lcProp.slice(0, directiveNameLength) === lcDirectiveName) {
3143
+ directiveOptions[_.kebabCase(prop.slice(directiveNameLength))] = deserialize(attr[prop]);
2425
3144
  }
2426
3145
  }
2427
- return { info: info, options: options, directiveOptions: directiveOptions };
2428
- },
2429
- buildInputMarkup: function buildInputMarkup(scope, model, info, options, addButtons, needsX, generateInputControl) {
2430
- var fieldChrome = formMarkupHelper.fieldChrome(scope, info, options, ' id="cg_' + info.id + '"');
2431
- var controlDivClasses = formMarkupHelper.controlDivClasses(options);
2432
- var elementHtml = fieldChrome.template + formMarkupHelper.label(scope, info, addButtons, options);
2433
- var modelString, idString, nameString;
2434
- if (addButtons) {
2435
- modelString = 'arrayItem' + (needsX ? '.x' : '');
2436
- idString = info.id + '_{{$index}}';
2437
- nameString = info.name + '_{{$index}}';
3146
+ }
3147
+ var result = { info: info, options: options, directiveOptions: directiveOptions };
3148
+ // any part of the help text or label that is surrounded by @@ @@ is assumed to be something that can have
3149
+ // a pseudonym. We'll be relying upon the parent controller assigning a pseudo() function to baseScope to
3150
+ // actually perform the translation.
3151
+ // TODO - do this better when fng is re-written!
3152
+ result.info.help = handlePseudos(result.info.help);
3153
+ result.info.label = handlePseudos(result.info.label);
3154
+ return result;
3155
+ }
3156
+ function genIdAndDisabledStr(scope, processedAttrs, idSuffix, params) {
3157
+ var idStr = internalGenIdString(scope, processedAttrs, idSuffix, false);
3158
+ var uniqueIdStr = makeIdStringUniqueForArrayElements(scope, processedAttrs, idStr);
3159
+ return "id=\"".concat(uniqueIdStr, "\" ").concat(internalGenDisabledStr(scope, idStr, processedAttrs, idSuffix, params));
3160
+ }
3161
+ return {
3162
+ extractFromAttr: extractFromAttr,
3163
+ buildInputMarkup: function buildInputMarkup(scope, attrs, params, generateInputControl) {
3164
+ var processedAttrs = params.processedAttrs || extractFromAttr(attrs, "");
3165
+ var info = {};
3166
+ if (!params.ignoreFieldInfoFromAttrs) {
3167
+ Object.assign(info, processedAttrs.info);
2438
3168
  }
2439
- else {
2440
- modelString = model + '.' + info.name;
2441
- idString = info.id;
2442
- nameString = info.name;
3169
+ if (params.fieldInfoOverrides) {
3170
+ Object.assign(info, params.fieldInfoOverrides);
3171
+ }
3172
+ var options = Object.assign({}, processedAttrs.options, params.optionOverrides);
3173
+ var fieldChrome = formMarkupHelper.fieldChrome(scope, info, options);
3174
+ if (fieldChrome.omit) {
3175
+ return "";
2443
3176
  }
2444
- if (options.subschema && info.name.indexOf('.') !== -1) {
3177
+ var controlDivClasses = formMarkupHelper.controlDivClasses(options);
3178
+ var elementHtml = fieldChrome.template + formMarkupHelper.label(scope, info, params.addButtons, options);
3179
+ var idString = info.id;
3180
+ if (info.array || options.subschema) {
3181
+ idString = formMarkupHelper.generateArrayElementIdString(idString, info, options);
3182
+ }
3183
+ var modelString = params.addButtons
3184
+ ? "arrayItem" + (params.needsX ? ".x" : "")
3185
+ : attrs.model + "." + info.name;
3186
+ var nameString = info.name;
3187
+ if (options.subschema && info.name.indexOf(".") !== -1) {
2445
3188
  // Schema handling - need to massage the ngModel and the id
2446
- var modelBase = model + '.';
2447
- var compoundName = info.name;
3189
+ var modelBase = attrs.model + ".";
2448
3190
  var root = options.subschemaroot;
2449
- var lastPart = compoundName.slice(root.length + 1);
2450
- modelString = modelBase;
2451
- if (options.index) {
2452
- modelString += root + '[' + options.index + '].' + lastPart;
2453
- idString = 'f_' + modelString.slice(modelBase.length).replace(/(\.|\[|\]\.)/g, '-');
3191
+ var lastPart = info.name.slice(root.length + 1);
3192
+ modelString = modelBase + root;
3193
+ if (options.subkey) {
3194
+ idString = modelString.slice(modelBase.length).replace(/\./g, "-") + "-subkey" + options.subkeyno + "-" + lastPart;
3195
+ modelString += "[" + "$_arrayOffset_" + root.replace(/\./g, "_") + "_" + options.subkeyno + "]." + lastPart;
2454
3196
  }
2455
3197
  else {
2456
- modelString += root;
2457
- if (options.subkey) {
2458
- idString = modelString.slice(modelBase.length).replace(/\./g, '-') + '-subkey' + options.subkeyno + '-' + lastPart;
2459
- modelString += '[' + '$_arrayOffset_' + root.replace(/\./g, '_') + '_' + options.subkeyno + '].' + lastPart;
2460
- }
2461
- else {
2462
- modelString += '[$index].' + lastPart;
2463
- idString = null;
2464
- nameString = compoundName.replace(/\./g, '-');
2465
- }
3198
+ modelString += "[$index]." + lastPart;
3199
+ nameString = info.name.replace(/\./g, "-");
2466
3200
  }
2467
3201
  }
2468
3202
  var buildingBlocks = formMarkupHelper.allInputsVars(scope, info, options, modelString, idString, nameString);
2469
3203
  buildingBlocks.modelString = modelString;
2470
- elementHtml += formMarkupHelper['handle' + (addButtons ? 'Array' : '') + 'InputAndControlDiv'](formMarkupHelper.inputChrome(generateInputControl(buildingBlocks), info, options, buildingBlocks), controlDivClasses, info, options);
3204
+ buildingBlocks.disableableAncestorStr = formMarkupHelper.genDisableableAncestorStr(info.id);
3205
+ // defer to the calling directive to generate the markup for the input(s)
3206
+ var inputHtml = generateInputControl(buildingBlocks);
3207
+ // wrap this in a div that puts it into the correct bootstrap 'column' and adds validation messages and help text
3208
+ var wrappedInputHtml = formMarkupHelper.inputChrome(inputHtml, info, options, buildingBlocks);
3209
+ // further wrap this to add the control div classes, and in the case of an array, the button that allows array elements to be removed
3210
+ if (params.addButtons) {
3211
+ elementHtml += formMarkupHelper.handleArrayInputAndControlDiv(wrappedInputHtml, controlDivClasses, scope, info, options);
3212
+ }
3213
+ else {
3214
+ elementHtml += formMarkupHelper.handleInputAndControlDiv(wrappedInputHtml, controlDivClasses);
3215
+ }
2471
3216
  elementHtml += fieldChrome.closeTag;
2472
3217
  return elementHtml;
2473
3218
  },
2474
- findIdInSchemaAndFlagNeedX: function findIdInSchemaAndFlagNeedX(scope, id) {
2475
- // Find the entry in the schema of scope for id and add a needsX property so string arrays are properly handled
2476
- var foundIt = false;
2477
- for (var i = 0; i < scope.length; i++) {
2478
- var element = scope[i];
2479
- if (element.id === id) {
2480
- element.needsX = true;
2481
- foundIt = true;
2482
- break;
2483
- }
2484
- else if (element.schema) {
2485
- if (findIdInSchemaAndFlagNeedX(element.schema, id)) {
2486
- foundIt = true;
2487
- break;
2488
- }
2489
- }
2490
- }
2491
- return foundIt;
3219
+ genIdString: function genIdString(scope, processedAttrs, idSuffix) {
3220
+ return internalGenIdString(scope, processedAttrs, idSuffix, true);
3221
+ },
3222
+ genDisabledStr: function genDisabledStr(scope, processedAttrs, idSuffix, params) {
3223
+ var idString = internalGenIdString(scope, processedAttrs, idSuffix, false);
3224
+ return internalGenDisabledStr(scope, idString, processedAttrs, idSuffix, params);
3225
+ },
3226
+ genIdAndDisabledStr: genIdAndDisabledStr,
3227
+ genDateTimePickerDisabledStr: function genDateTimePickerDisabledStr(scope, processedAttrs, idSuffix) {
3228
+ var idString = internalGenIdString(scope, processedAttrs, idSuffix, false);
3229
+ return internalGenDateTimePickerDisabledStr(scope, processedAttrs, idSuffix, idString);
3230
+ },
3231
+ genDateTimePickerIdAndDisabledStr: function genDateTimePickerIdAndDisabledStr(scope, processedAttrs, idSuffix) {
3232
+ var idStr = internalGenIdString(scope, processedAttrs, idSuffix, false);
3233
+ var uniqueIdStr = makeIdStringUniqueForArrayElements(scope, processedAttrs, idStr);
3234
+ return "id=\"".concat(uniqueIdStr, "\" ").concat(internalGenDateTimePickerDisabledStr(scope, processedAttrs, idSuffix, idStr));
3235
+ },
3236
+ genUiSelectIdAndDisabledStr: function genUiSelectIdAndDisabledStr(scope, processedAttrs, idSuffix) {
3237
+ // ui-select won't be disabled when a simple DISABLED attribute is provided - it requires
3238
+ // ng-disabled even when the value is simply "true"
3239
+ return genIdAndDisabledStr(scope, processedAttrs, idSuffix, { forceNg: true });
3240
+ },
3241
+ handlePseudos: handlePseudos,
3242
+ genDisableableAncestorStr: function genDisableableAncestorStr(processedAttrs) {
3243
+ return formMarkupHelper.genDisableableAncestorStr(processedAttrs.info.id);
2492
3244
  }
2493
3245
  };
2494
3246
  }
@@ -2507,26 +3259,32 @@ var fng;
2507
3259
  *
2508
3260
  */
2509
3261
  /*@ngInject*/
2510
- recordHandler.$inject = ["$http", "$location", "$window", "$filter", "$timeout", "routingService", "SubmissionsService", "SchemasService"];
2511
- function recordHandler($http, $location, $window, $filter, $timeout, routingService, SubmissionsService, SchemasService) {
3262
+ recordHandler.$inject = ["$location", "$window", "$filter", "$timeout", "$sce", "routingService", "cssFrameworkService", "SubmissionsService", "SchemasService"];
3263
+ function recordHandler($location, $window, $filter, $timeout, $sce, routingService, cssFrameworkService, SubmissionsService, SchemasService) {
2512
3264
  // TODO: Put this in a service
2513
3265
  var makeMongoId = function (rnd) {
2514
3266
  if (rnd === void 0) { rnd = function (r16) { return Math.floor(r16).toString(16); }; }
2515
- return rnd(Date.now() / 1000) + ' '.repeat(16).replace(/./g, function () { return rnd(Math.random() * 16); });
3267
+ return rnd(Date.now() / 1000) + " ".repeat(16).replace(/./g, function () { return rnd(Math.random() * 16); });
2516
3268
  };
3269
+ function _handleCancel(resp) {
3270
+ if (["cancel", "backdrop click", "escape key press"].indexOf(resp) === -1) {
3271
+ throw resp;
3272
+ }
3273
+ }
2517
3274
  var suffixCleanId = function suffixCleanId(inst, suffix) {
2518
- return (inst.id || 'f_' + inst.name).replace(/\./g, '_') + suffix;
3275
+ return (inst.id || "f_" + inst.name).replace(/\./g, "_") + suffix;
2519
3276
  };
2520
- var walkTree = function (object, fieldname, element) {
3277
+ var walkTree = function (object, fieldname, element, insertIntermediateObjects) {
2521
3278
  // Walk through subdocs to find the required key
2522
3279
  // for instance walkTree(master,'address.street.number',element)
2523
3280
  // called by getData and setData
3281
+ if (insertIntermediateObjects === void 0) { insertIntermediateObjects = false; }
2524
3282
  // element is used when accessing in the context of a input, as the id (like exams-2-grader)
2525
3283
  // gives us the element of an array (one level down only for now). Leaving element blank returns the whole array
2526
- var parts = fieldname.split('.'), higherLevels = parts.length - 1, workingRec = object;
3284
+ var parts = fieldname.split("."), higherLevels = parts.length - 1, workingRec = object;
2527
3285
  for (var i = 0; i < higherLevels; i++) {
2528
3286
  if (!workingRec) {
2529
- throw new Error("walkTree failed: Object = " + object + ", fieldname = " + fieldname + ", i = " + i);
3287
+ throw new Error("walkTree failed: Object = ".concat(object, ", fieldname = ").concat(fieldname, ", i = ").concat(i));
2530
3288
  }
2531
3289
  if (angular.isArray(workingRec)) {
2532
3290
  workingRec = _.map(workingRec, function (obj) {
@@ -2534,18 +3292,21 @@ var fng;
2534
3292
  });
2535
3293
  }
2536
3294
  else {
3295
+ if (insertIntermediateObjects && !workingRec[parts[i]]) {
3296
+ workingRec[parts[i]] = {};
3297
+ }
2537
3298
  workingRec = workingRec[parts[i]];
2538
3299
  }
2539
- if (angular.isArray(workingRec) && typeof element !== 'undefined') {
2540
- if (element.scope && typeof element.scope === 'function') {
3300
+ if (angular.isArray(workingRec) && typeof element !== "undefined") {
3301
+ if (element.scope && typeof element.scope === "function") {
2541
3302
  // If we come across an array we need to find the correct position, if we have an element
2542
3303
  workingRec = workingRec[element.scope().$index];
2543
3304
  }
2544
- else if (typeof element === 'number') {
3305
+ else if (typeof element === "number") {
2545
3306
  workingRec = workingRec[element];
2546
3307
  }
2547
3308
  else {
2548
- throw new Error('Unsupported element type in walkTree ' + fieldname);
3309
+ throw new Error("Unsupported element type in walkTree " + fieldname);
2549
3310
  }
2550
3311
  }
2551
3312
  if (!workingRec) {
@@ -2558,15 +3319,20 @@ var fng;
2558
3319
  };
2559
3320
  };
2560
3321
  var setData = function setData(object, fieldname, element, value) {
2561
- var leafData = walkTree(object, fieldname, element);
3322
+ var leafData = walkTree(object, fieldname, element, !!value);
2562
3323
  if (leafData.lastObject && leafData.key) {
2563
- if (angular.isArray(leafData.lastObject)) {
2564
- for (var i = 0; i < leafData.lastObject.length; i++) {
2565
- leafData.lastObject[i][leafData.key] = value[i];
3324
+ if (value) {
3325
+ if (angular.isArray(leafData.lastObject)) {
3326
+ for (var i = 0; i < leafData.lastObject.length; i++) {
3327
+ leafData.lastObject[i][leafData.key] = value[i];
3328
+ }
3329
+ }
3330
+ else {
3331
+ leafData.lastObject[leafData.key] = value;
2566
3332
  }
2567
3333
  }
2568
3334
  else {
2569
- leafData.lastObject[leafData.key] = value;
3335
+ delete leafData.lastObject[leafData.key];
2570
3336
  }
2571
3337
  }
2572
3338
  };
@@ -2585,11 +3351,17 @@ var fng;
2585
3351
  }
2586
3352
  return retVal;
2587
3353
  };
2588
- var updateRecordWithLookupValues = function (schemaElement, $scope, ctrlState) {
3354
+ var updateRecordWithLookupValues = function (schemaElement, $scope, ctrlState, ignoreDirty) {
3355
+ if (ignoreDirty === void 0) { ignoreDirty = false; }
2589
3356
  // Update the master and the record with the lookup values, master first
2590
- if (!$scope.topLevelFormName || $scope[$scope.topLevelFormName].$pristine) {
3357
+ if (!$scope.topLevelFormName || ($scope[$scope.topLevelFormName] && (ignoreDirty || $scope[$scope.topLevelFormName].$pristine))) {
2591
3358
  updateObject(schemaElement.name, ctrlState.master, function (value) {
2592
- return convertForeignKeys(schemaElement, value, $scope[suffixCleanId(schemaElement, 'Options')], $scope[suffixCleanId(schemaElement, '_ids')]);
3359
+ if (typeof value == "object" && value.id) {
3360
+ return value;
3361
+ }
3362
+ else {
3363
+ return convertForeignKeys(schemaElement, value, $scope[suffixCleanId(schemaElement, "Options")], $scope[suffixCleanId(schemaElement, "_ids")]);
3364
+ }
2593
3365
  });
2594
3366
  // Then copy the converted keys from master into record
2595
3367
  var newVal = getData(ctrlState.master, schemaElement.name);
@@ -2600,38 +3372,29 @@ var fng;
2600
3372
  };
2601
3373
  // Split a field name into the next level and all following levels
2602
3374
  function splitFieldName(aFieldName) {
2603
- var nesting = aFieldName.split('.'), result = [nesting[0]];
3375
+ var nesting = aFieldName.split("."), result = [nesting[0]];
2604
3376
  if (nesting.length > 1) {
2605
- result.push(nesting.slice(1).join('.'));
3377
+ result.push(nesting.slice(1).join("."));
2606
3378
  }
2607
3379
  return result;
2608
3380
  }
2609
- var getListData = function getListData($scope, record, fieldName, listSchema) {
3381
+ var getListData = function getListData(record, fieldName, listSchema, $scope) {
2610
3382
  if (listSchema === void 0) { listSchema = null; }
2611
- var retVal = record;
2612
- var nests = fieldName.split('.');
2613
- for (var i = 0; i < nests.length; i++) {
2614
- if (retVal !== undefined && retVal !== null && nests && nests[i]) {
2615
- retVal = retVal[nests[i]];
2616
- }
2617
- }
2618
- if (retVal === undefined) {
2619
- retVal = '';
2620
- }
3383
+ var retVal = getData(record, fieldName) || "";
2621
3384
  if (retVal && listSchema) {
2622
3385
  // Convert list fields as per instructions in params (ideally should be the same as what is found in data_form getListFields
2623
- var schemaElm = _.find(listSchema, function (elm) { return (elm['name'] === fieldName); });
3386
+ var schemaElm = _.find(listSchema, function (elm) { return (elm["name"] === fieldName); });
2624
3387
  if (schemaElm) {
2625
- switch (schemaElm['params']) {
3388
+ switch (schemaElm["params"]) {
2626
3389
  case undefined:
2627
3390
  break;
2628
- case 'timestamp':
3391
+ case "timestamp":
2629
3392
  var timestamp = retVal.toString().substring(0, 8);
2630
3393
  var date = new Date(parseInt(timestamp, 16) * 1000);
2631
- retVal = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
3394
+ retVal = date.toLocaleDateString() + " " + date.toLocaleTimeString();
2632
3395
  break;
2633
3396
  default:
2634
- retVal = $scope.dataEventFunctions[schemaElm['params']](record);
3397
+ retVal = $scope.dataEventFunctions[schemaElm["params"]](record);
2635
3398
  }
2636
3399
  }
2637
3400
  }
@@ -2648,7 +3411,7 @@ var fng;
2648
3411
  if (angular.isArray(theValue)) {
2649
3412
  for (var i = theValue.length - 1; i >= 0; i--) {
2650
3413
  var type = typeof theValue[i];
2651
- if (type === 'undefined' || (type === 'object' && Object.keys(theValue[i]).length === 0)) {
3414
+ if (type === "undefined" || (type === "object" && Object.keys(theValue[i]).length === 0)) {
2652
3415
  theValue.splice(i, 1);
2653
3416
  }
2654
3417
  }
@@ -2670,8 +3433,8 @@ var fng;
2670
3433
  }
2671
3434
  // Set up the lookup lists (value and id) on the scope for an internal lookup. Called by convertToAngularModel and $watch
2672
3435
  function setUpInternalLookupLists($scope, options, ids, newVal, valueAttrib) {
2673
- var optionsArray = (typeof options === 'string' ? $scope[options] : options);
2674
- var idsArray = (typeof ids === 'string' ? $scope[ids] : ids);
3436
+ var optionsArray = (typeof options === "string" ? $scope[options] : options);
3437
+ var idsArray = (typeof ids === "string" ? $scope[ids] : ids);
2675
3438
  optionsArray.length = 0;
2676
3439
  idsArray.length = 0;
2677
3440
  if (!!newVal && (newVal.length > 0)) {
@@ -2693,10 +3456,10 @@ var fng;
2693
3456
  result = true;
2694
3457
  }
2695
3458
  else if (!aSchema.directive) {
2696
- if (aSchema.type === 'text') {
3459
+ if (aSchema.type === "text") {
2697
3460
  result = true;
2698
3461
  }
2699
- else if (aSchema.type === 'select' && !aSchema.ids) {
3462
+ else if (aSchema.type === "select" && !aSchema.ids) {
2700
3463
  result = true;
2701
3464
  }
2702
3465
  }
@@ -2706,7 +3469,7 @@ var fng;
2706
3469
  function getConversionObject(scope, entryName, schemaName) {
2707
3470
  var conversions = scope.conversions;
2708
3471
  if (schemaName) {
2709
- conversions = conversions[schemaName] || {};
3472
+ conversions = getData(conversions, schemaName) || {};
2710
3473
  }
2711
3474
  return conversions[entryName];
2712
3475
  }
@@ -2717,46 +3480,62 @@ var fng;
2717
3480
  for (var i = 0; i < schema.length; i++) {
2718
3481
  var schemaEntry = schema[i];
2719
3482
  var fieldName = schemaEntry.name.slice(prefixLength);
3483
+ if (!fieldName.length) {
3484
+ fieldName = schemaEntry.name.split('.').pop();
3485
+ }
2720
3486
  var fieldValue = getData(anObject, fieldName);
3487
+ if (schemaEntry.intType === 'date' && typeof fieldValue === 'string') {
3488
+ setData(anObject, fieldName, null, new Date(fieldValue));
3489
+ }
2721
3490
  if (schemaEntry.schema) {
2722
3491
  if (fieldValue) {
2723
3492
  for (var j = 0; j < fieldValue.length; j++) {
2724
- fieldValue[j] = convertToAngularModel(schemaEntry.schema, fieldValue[j], prefixLength + 1 + fieldName.length, $scope, fieldName, master, j);
3493
+ fieldValue[j] = convertToAngularModel(schemaEntry.schema, fieldValue[j], 1 + fieldName.length, $scope, fieldName, master, j);
2725
3494
  }
2726
3495
  }
2727
3496
  }
2728
3497
  else {
2729
- if (schemaEntry.ref && schemaEntry.ref.type === 'internal') {
2730
- setUpInternalLookupLists($scope, schemaEntry.options, schemaEntry.ids, master[schemaEntry.ref.property], schemaEntry.ref.value);
3498
+ if (schemaEntry.internalRef) {
3499
+ setUpInternalLookupLists($scope, schemaEntry.options, schemaEntry.ids, master[schemaEntry.internalRef.property], schemaEntry.internalRef.value);
2731
3500
  }
2732
3501
  // Convert {array:['item 1']} to {array:[{x:'item 1'}]}
2733
- var thisField = getListData($scope, anObject, fieldName);
2734
- if (schemaEntry.array && simpleArrayNeedsX(schemaEntry) && thisField) {
3502
+ var thisField = getListData(anObject, fieldName, null, $scope);
3503
+ if (schemaEntry.array &&
3504
+ simpleArrayNeedsX(schemaEntry) &&
3505
+ thisField &&
3506
+ !(thisField.length > 0 && thisField[0].x) // Don't keep on coverting
3507
+ ) {
2735
3508
  for (var k = 0; k < thisField.length; k++) {
2736
3509
  thisField[k] = { x: thisField[k] };
2737
3510
  }
2738
3511
  }
2739
3512
  // Convert {lookup:'012abcde'} to {lookup:'List description for 012abcde'}
2740
- var idList = $scope[suffixCleanId(schemaEntry, '_ids')];
3513
+ var idList = $scope[suffixCleanId(schemaEntry, "_ids")];
2741
3514
  var thisConversion = void 0;
2742
3515
  if (fieldValue && idList && idList.length > 0) {
2743
- if (fieldName.indexOf('.') !== -1) {
2744
- throw new Error('Trying to directly assign to a nested field 332');
2745
- } // Not sure that this can happen, but put in a runtime test
2746
- anObject[fieldName] = convertForeignKeys(schemaEntry, fieldValue, $scope[suffixCleanId(schemaEntry, 'Options')], idList);
3516
+ if (
3517
+ // it's not a nested field
3518
+ !fieldName.includes(".") &&
3519
+ // Check we are starting with an ObjectId (ie not being called because of $watch on conversion, with a converted value, which would cause an exception)
3520
+ fieldValue.toString().match(/^[a-f0-9]{24}$/) &&
3521
+ // We are not suppressing conversions
3522
+ (!schemaEntry.internalRef || !schemaEntry.internalRef.noConvert)) {
3523
+ anObject[fieldName] = convertForeignKeys(schemaEntry, fieldValue, $scope[suffixCleanId(schemaEntry, "Options")], idList);
3524
+ }
2747
3525
  }
2748
3526
  else if (schemaEntry.select2) {
2749
3527
  // Do nothing with these - handled elsewhere (and deprecated)
2750
- console.log('fng-select2 is deprecated - use fng-ui-select instead');
3528
+ console.log("fng-select2 is deprecated - use fng-ui-select instead");
2751
3529
  void (schemaEntry.select2);
2752
3530
  }
2753
3531
  else if (fieldValue && (thisConversion = getConversionObject($scope, fieldName, schemaName)) &&
2754
3532
  thisConversion.fngajax &&
3533
+ typeof thisConversion.fngajax === "function" && // if the field is securely hidden, the directive won't have been invoked at all and therefore the conversion will not have been initialised. but if it's hidden, we don't need to do the conversion anyway
2755
3534
  !thisConversion.noconvert) {
2756
3535
  thisConversion.fngajax(fieldValue, schemaEntry, function (updateEntry, value) {
2757
3536
  // Update the master and (preserving pristine if appropriate) the record
2758
3537
  setData(master, updateEntry.name, offset, value);
2759
- preservePristine(angular.element('#' + updateEntry.id), function () {
3538
+ preservePristine(angular.element("#" + updateEntry.id), function () {
2760
3539
  setData($scope.record, updateEntry.name, offset, value);
2761
3540
  });
2762
3541
  });
@@ -2769,9 +3548,9 @@ var fng;
2769
3548
  // Called when the model is read and when the lookups are read
2770
3549
  // No support for nested schemas here as it is called from convertToAngularModel which does that
2771
3550
  function convertForeignKeys(schemaElement, input, values, ids) {
2772
- if (schemaElement.array) {
3551
+ if (schemaElement.array || angular.isArray(input)) {
2773
3552
  var returnArray = [];
2774
- var needsX = !schemaElement.directive || simpleArrayNeedsX(schemaElement);
3553
+ var needsX = schemaElement.array && (!schemaElement.directive || simpleArrayNeedsX(schemaElement));
2775
3554
  for (var j = 0; j < input.length; j++) {
2776
3555
  var val = input[j];
2777
3556
  if (val && val.x) {
@@ -2815,7 +3594,7 @@ var fng;
2815
3594
  else {
2816
3595
  var index = valuesArray.indexOf(textToConvert);
2817
3596
  if (index === -1) {
2818
- throw new Error('convertListValueToId: Invalid data - value ' + textToConvert + ' not found in ' + valuesArray + ' processing ' + fname);
3597
+ throw new Error("convertListValueToId: Invalid data - value " + textToConvert + " not found in " + valuesArray + " processing " + fname);
2819
3598
  }
2820
3599
  return idsArray[index];
2821
3600
  }
@@ -2823,7 +3602,7 @@ var fng;
2823
3602
  var preservePristine = function preservePristine(element, fn) {
2824
3603
  // stop the form being set to dirty when a fn is called
2825
3604
  // Use when the record (and master) need to be updated by lookup values displayed asynchronously
2826
- var modelController = element.inheritedData('$ngModelController');
3605
+ var modelController = element.inheritedData("$ngModelController");
2827
3606
  var isClean = (modelController && modelController.$pristine);
2828
3607
  if (isClean) {
2829
3608
  // fake it to dirty here and reset after call to fn
@@ -2835,18 +3614,21 @@ var fng;
2835
3614
  }
2836
3615
  };
2837
3616
  var convertIdToListValue = function convertIdToListValue(id, idsArray, valuesArray, fname) {
2838
- if (typeof (id) === 'object') {
3617
+ if (typeof (id) === "object") {
2839
3618
  id = id.id;
2840
3619
  }
2841
3620
  var index = idsArray.indexOf(id);
2842
3621
  if (index === -1) {
2843
- throw new Error('convertIdToListValue: Invalid data - id ' + id + ' not found in ' + idsArray + ' processing ' + fname);
3622
+ index = valuesArray.indexOf(id); // This can get called twice - second time with converted value (not sure how atm) so protect against that...
3623
+ if (index === -1) {
3624
+ throw new Error("convertIdToListValue: Invalid data - id " + id + " not found in " + idsArray + " processing " + fname);
3625
+ }
2844
3626
  }
2845
3627
  return valuesArray[index];
2846
3628
  };
2847
3629
  var processServerData = function processServerData(recordFromServer, $scope, ctrlState) {
2848
3630
  ctrlState.master = convertToAngularModel($scope.formSchema, recordFromServer, 0, $scope);
2849
- $scope.phase = 'ready';
3631
+ $scope.phase = "ready";
2850
3632
  $scope.cancel();
2851
3633
  };
2852
3634
  function convertOldToNew(ref, val, attrib, newVals, oldVals) {
@@ -2865,7 +3647,7 @@ var fng;
2865
3647
  var listOnly = (!$scope.id && !$scope.newRecord);
2866
3648
  // passing null for formSchema parameter prevents all the work being done when we are just after the list data,
2867
3649
  // but should be removed when/if formschemas are cached
2868
- formGeneratorInstance.handleSchema('Main ' + $scope.modelName, schema, listOnly ? null : $scope.formSchema, $scope.listSchema, '', true, $scope, ctrlState);
3650
+ formGeneratorInstance.handleSchema("Main " + $scope.modelName, schema, listOnly ? null : $scope.formSchema, $scope.listSchema, "", true, $scope, ctrlState);
2869
3651
  function processLookupHandlers(newValue, oldValue) {
2870
3652
  // If we have any internal lookups then update the references
2871
3653
  $scope.internalLookups.forEach(function (lkp) {
@@ -2901,18 +3683,36 @@ var fng;
2901
3683
  }
2902
3684
  return retVal;
2903
3685
  }
3686
+ function blankListLookup(inst) {
3687
+ setData($scope.record, inst.name);
3688
+ }
2904
3689
  var idString = lkp.ref.id.slice(1);
2905
3690
  if (idString.includes(".")) {
2906
- throw new Error("No support for nested list lookups yet - " + JSON.stringify(lkp.ref));
3691
+ throw new Error("No support for nested list lookups yet - ".concat(JSON.stringify(lkp.ref)));
2907
3692
  }
2908
3693
  var newVal = extractIdVal(newValue, idString);
2909
3694
  var oldVal = extractIdVal(oldValue, idString);
2910
3695
  if (newVal !== oldVal) {
2911
3696
  if (newVal) {
3697
+ if (oldVal) {
3698
+ lkp.handlers.forEach(function (h) {
3699
+ h.oldValue = getData($scope.record, h.formInstructions.name);
3700
+ if (angular.isArray(h.oldValue)) {
3701
+ h.oldId = h.oldValue.map(function (a) {
3702
+ return $scope[h.formInstructions.ids][$scope[h.formInstructions.options].indexOf(a)];
3703
+ });
3704
+ }
3705
+ else {
3706
+ h.oldId = $scope[h.formInstructions.ids][$scope[h.formInstructions.options].indexOf(h.oldValue)];
3707
+ }
3708
+ });
3709
+ }
2912
3710
  SubmissionsService.readRecord(lkp.ref.collection, newVal).then(function (response) {
2913
3711
  lkp.handlers.forEach(function (h) {
2914
3712
  var optionsList = $scope[h.formInstructions.options];
3713
+ optionsList.length = 0;
2915
3714
  var idList = $scope[h.formInstructions.ids];
3715
+ idList.length = 0;
2916
3716
  var data = response.data[lkp.ref.property] || [];
2917
3717
  for (var i = 0; i < data.length; i++) {
2918
3718
  var option = data[i][lkp.ref.value];
@@ -2925,7 +3725,33 @@ var fng;
2925
3725
  optionsList.splice(pos, 0, option);
2926
3726
  idList.splice(pos, 0, data[i]._id);
2927
3727
  }
2928
- updateRecordWithLookupValues(h.formInstructions, $scope, ctrlState);
3728
+ if (Object.keys(oldValue).length === 0) {
3729
+ // Not sure how safe this is, but the record is fresh so I think it's OK...
3730
+ updateRecordWithLookupValues(h.formInstructions, $scope, ctrlState, true);
3731
+ }
3732
+ else if (h.oldId) {
3733
+ // Here we are reacting to a change in the lookup pointer in the record.
3734
+ // If the old id exists in the new idList we can keep it, otherwise we need to blank it.
3735
+ // We need to remember that we can have an array of ids
3736
+ if (angular.isArray(h.oldId)) {
3737
+ h.oldId.forEach(function (id, idx) {
3738
+ var pos = idList.indexOf(id);
3739
+ setData($scope.record, h.formInstructions.name, idx, pos === -1 ? undefined : optionsList[pos]);
3740
+ });
3741
+ }
3742
+ else {
3743
+ var pos_1 = idList.indexOf(h.oldId);
3744
+ if (pos_1 !== -1) {
3745
+ setData($scope.record, h.formInstructions.name, undefined, optionsList[pos_1]);
3746
+ }
3747
+ else {
3748
+ blankListLookup(h.formInstructions);
3749
+ }
3750
+ }
3751
+ }
3752
+ else {
3753
+ blankListLookup(h.formInstructions);
3754
+ }
2929
3755
  });
2930
3756
  });
2931
3757
  }
@@ -2933,14 +3759,14 @@ var fng;
2933
3759
  lkp.handlers.forEach(function (h) {
2934
3760
  $scope[h.formInstructions.options].length = 0;
2935
3761
  $scope[h.formInstructions.ids].length = 0;
2936
- updateRecordWithLookupValues(h.formInstructions, $scope, ctrlState);
3762
+ blankListLookup(h.formInstructions);
2937
3763
  });
2938
3764
  }
2939
3765
  }
2940
3766
  });
2941
3767
  }
2942
3768
  function notifyReady() {
2943
- $scope.phase = 'ready';
3769
+ $scope.phase = "ready";
2944
3770
  $scope.cancel();
2945
3771
  processLookupHandlers($scope.record, {});
2946
3772
  }
@@ -2950,13 +3776,13 @@ var fng;
2950
3776
  else {
2951
3777
  var force = true;
2952
3778
  if (!$scope.newRecord) {
2953
- $scope.dropConversionWatcher = $scope.$watchCollection('conversions', function (newValue, oldValue) {
3779
+ $scope.dropConversionWatcher = $scope.$watchCollection("conversions", function (newValue, oldValue) {
2954
3780
  if (newValue !== oldValue && $scope.originalData) {
2955
3781
  processServerData($scope.originalData, $scope, ctrlState);
2956
3782
  }
2957
3783
  });
2958
3784
  }
2959
- $scope.$watch('record', function (newValue, oldValue) {
3785
+ $scope.$watch("record", function (newValue, oldValue) {
2960
3786
  if (newValue !== oldValue) {
2961
3787
  if (Object.keys(oldValue).length > 0 && $scope.dropConversionWatcher) {
2962
3788
  $scope.dropConversionWatcher(); // Don't want to convert changed data
@@ -2964,11 +3790,27 @@ var fng;
2964
3790
  }
2965
3791
  force = formGeneratorInstance.updateDataDependentDisplay(newValue, oldValue, force, $scope);
2966
3792
  processLookupHandlers(newValue, oldValue);
3793
+ if (fng.formsAngular.title) {
3794
+ var title = fng.formsAngular.title.prefix || '';
3795
+ if ($scope['editFormHeader']) {
3796
+ title += $scope['editFormHeader']();
3797
+ }
3798
+ else {
3799
+ for (var listElm in $scope.listSchema) {
3800
+ if ($scope.listSchema.hasOwnProperty(listElm)) {
3801
+ title += $scope.getListData($scope.record, $scope.listSchema[listElm].name) + ' ';
3802
+ }
3803
+ }
3804
+ }
3805
+ title = title.trimEnd() + (fng.formsAngular.title.suffix || '');
3806
+ $window.document.title = title.replace(/<\/?[^>]+(>|$)/g, "");
3807
+ ;
3808
+ }
2967
3809
  }
2968
3810
  }, true);
2969
3811
  if ($scope.id) {
2970
3812
  // Going to read a record
2971
- if (typeof $scope.dataEventFunctions.onBeforeRead === 'function') {
3813
+ if (typeof $scope.dataEventFunctions.onBeforeRead === "function") {
2972
3814
  $scope.dataEventFunctions.onBeforeRead($scope.id, function (err) {
2973
3815
  if (err) {
2974
3816
  $scope.showError(err);
@@ -2984,29 +3826,32 @@ var fng;
2984
3826
  }
2985
3827
  else {
2986
3828
  // New record
2987
- ctrlState.master = {};
3829
+ ctrlState.allowLocationChange = false;
3830
+ ctrlState.master = $scope.setDefaults($scope.formSchema);
2988
3831
  var passedRecord = $scope.initialiseNewRecord || $location.$$search.r;
2989
3832
  if (passedRecord) {
2990
3833
  try {
2991
- ctrlState.master = JSON.parse(passedRecord);
2992
- // Although this is a new record we are making it dirty from the url so we need to $setDirty
2993
- $scope.$on('fngCancel', function () {
2994
- setTimeout(function () {
2995
- if ($scope[$scope.topLevelFormName]) {
2996
- $scope[$scope.topLevelFormName].$setDirty();
2997
- }
2998
- }, 2); // Has to fire after the setPristime timeout.
2999
- });
3834
+ Object.assign(ctrlState.master, JSON.parse(passedRecord));
3835
+ if (!$scope["newRecordsStartPristine"]) {
3836
+ // Although this is a new record we are making it dirty from the url so we need to $setDirty
3837
+ $scope.$on("fngCancel", function () {
3838
+ $timeout(function () {
3839
+ if ($scope[$scope.topLevelFormName]) {
3840
+ $scope[$scope.topLevelFormName].$setDirty();
3841
+ }
3842
+ }, 1000); // Has to fire after the setPristime timeout.
3843
+ });
3844
+ }
3000
3845
  }
3001
3846
  catch (e) {
3002
- console.log('Error parsing specified record : ' + e.message);
3847
+ console.log("Error parsing specified record : " + e.message);
3003
3848
  }
3004
3849
  }
3005
- if (typeof $scope.dataEventFunctions.onInitialiseNewRecord === 'function') {
3006
- console.log('onInitialiseNewRecord is deprecated - use the async version - onNewRecordInit(data,cb)');
3850
+ if (typeof $scope.dataEventFunctions.onInitialiseNewRecord === "function") {
3851
+ console.log("onInitialiseNewRecord is deprecated - use the async version - onNewRecordInit(data,cb)");
3007
3852
  $scope.dataEventFunctions.onInitialiseNewRecord(ctrlState.master);
3008
3853
  }
3009
- if (typeof $scope.dataEventFunctions.onNewRecordInit === 'function') {
3854
+ if (typeof $scope.dataEventFunctions.onNewRecordInit === "function") {
3010
3855
  $scope.dataEventFunctions.onNewRecordInit(ctrlState.master, function (err) {
3011
3856
  if (err) {
3012
3857
  $scope.showError(err);
@@ -3024,71 +3869,92 @@ var fng;
3024
3869
  }
3025
3870
  function handleError($scope) {
3026
3871
  return function (response) {
3027
- if ([200, 400].indexOf(response.status) !== -1) {
3028
- var errorMessage = '';
3029
- for (var errorField in response.data.errors) {
3030
- if (response.data.errors.hasOwnProperty(errorField)) {
3031
- errorMessage += '<li><b>' + $filter('titleCase')(errorField) + ': </b> ';
3032
- switch (response.data.errors[errorField].type) {
3033
- case 'enum':
3034
- errorMessage += 'You need to select from the list of values';
3035
- break;
3036
- default:
3037
- errorMessage += response.data.errors[errorField].message;
3038
- break;
3039
- }
3040
- errorMessage += '</li>';
3872
+ if ([200, 400, 403].indexOf(response.status) !== -1) {
3873
+ var errorMessage = "";
3874
+ if (response.data && response.data.errors) {
3875
+ for (var errorField in response.data.errors) {
3876
+ if (response.data.errors.hasOwnProperty(errorField)) {
3877
+ errorMessage += "<li><b>" + $filter("titleCase")(errorField) + ": </b> ";
3878
+ switch (response.data.errors[errorField].type) {
3879
+ case "enum":
3880
+ errorMessage += "You need to select from the list of values";
3881
+ break;
3882
+ default:
3883
+ errorMessage += response.data.errors[errorField].message;
3884
+ break;
3885
+ }
3886
+ errorMessage += "</li>";
3887
+ }
3041
3888
  }
3042
3889
  }
3043
3890
  if (errorMessage.length > 0) {
3044
- errorMessage = response.data.message + '<br /><ul>' + errorMessage + '</ul>';
3891
+ errorMessage = (response.data.message || response.data._message) + "<br /><ul>" + errorMessage + "</ul>";
3045
3892
  }
3046
3893
  else {
3047
- errorMessage = response.data.message || 'Error! Sorry - No further details available.';
3894
+ errorMessage = response.data.message || response.data._message || response.data.err || "Error! Sorry - No further details available.";
3048
3895
  }
3896
+ // anyone using a watch on $scope.phase, and waiting for it to become "ready" before proceeding, will probably
3897
+ // want to know that an error has occurred. This value is NOT used anywhere in forms-angular.
3898
+ $scope.phase = "error";
3049
3899
  $scope.showError(errorMessage);
3050
3900
  }
3051
3901
  else {
3052
- $scope.showError(response.status + ' ' + JSON.stringify(response.data));
3902
+ $scope.showError(response.status + " " + JSON.stringify(response.data));
3053
3903
  }
3054
3904
  };
3055
3905
  }
3056
3906
  function handleIncomingData(data, $scope, ctrlState) {
3057
3907
  ctrlState.allowLocationChange = false;
3058
- $scope.phase = 'reading';
3059
- if (typeof $scope.dataEventFunctions.onAfterRead === 'function') {
3908
+ $scope.phase = "reading";
3909
+ if (typeof $scope.dataEventFunctions.onAfterRead === "function") {
3060
3910
  $scope.dataEventFunctions.onAfterRead(data);
3061
3911
  }
3062
3912
  $scope.originalData = data;
3063
3913
  processServerData(data, $scope, ctrlState);
3064
3914
  }
3915
+ function addArrayLookupToLookupList($scope, formInstructions, ref, lookups) {
3916
+ var nameElements = formInstructions.name.split(".");
3917
+ var refHandler = lookups.find(function (lkp) {
3918
+ return lkp.ref.property === ref.property && lkp.ref.value === ref.value;
3919
+ });
3920
+ var thisHandler = {
3921
+ formInstructions: formInstructions,
3922
+ lastPart: nameElements.pop(),
3923
+ possibleArray: nameElements.join(".")
3924
+ };
3925
+ if (!refHandler) {
3926
+ refHandler = {
3927
+ ref: ref,
3928
+ lookupOptions: [],
3929
+ lookupIds: [],
3930
+ handlers: []
3931
+ };
3932
+ lookups.push(refHandler);
3933
+ }
3934
+ refHandler.handlers.push(thisHandler);
3935
+ $scope[formInstructions.options] = refHandler.lookupOptions;
3936
+ $scope[formInstructions.ids] = refHandler.lookupIds;
3937
+ }
3065
3938
  return {
3066
3939
  readRecord: function readRecord($scope, ctrlState) {
3067
- // TODO Consider using $parse for this - http://bahmutov.calepin.co/angularjs-parse-hacks.html
3068
- SubmissionsService.readRecord($scope.modelName, $scope.id)
3940
+ $scope.readingRecord = SubmissionsService.readRecord($scope.modelName, $scope.id, $scope.formName);
3941
+ $scope.readingRecord
3069
3942
  .then(function (response) {
3070
- var data = response.data;
3071
- if (data.success === false) {
3072
- $location.path('/404');
3073
- // TODO Figure out tab history updates (check for other tab-history-todos)
3074
- // } else if (response.master) {
3075
- //
3076
- // ctrlState.allowLocationChange = false;
3077
- // $scope.phase = 'ready';
3078
- // $scope.record = angular.copy(response.data);
3079
- // ctrlState.master = angular.copy(response.master);
3080
- // if (response.changed) {
3081
- // $timeout(() => {
3082
- // $scope[$scope.topLevelFormName].$setDirty();
3083
- // });
3084
- // } else {
3085
- // $timeout($scope.setPristine);
3086
- // }
3943
+ var data = angular.copy(response.data);
3944
+ handleIncomingData(data, $scope, ctrlState);
3945
+ }, function (error) {
3946
+ if ($scope.dataEventFunctions.onReadError) {
3947
+ $scope.dataEventFunctions.onReadError($scope.id, error);
3087
3948
  }
3088
3949
  else {
3089
- handleIncomingData(data, $scope, ctrlState);
3950
+ if (error.status === 404) {
3951
+ $location.path("/404");
3952
+ }
3953
+ else {
3954
+ $scope.handleHttpError(error);
3955
+ }
3090
3956
  }
3091
- }, $scope.handleHttpError);
3957
+ });
3092
3958
  },
3093
3959
  scrollTheList: function scrollTheList($scope) {
3094
3960
  var pagesLoaded = $scope.pagesLoaded;
@@ -3097,42 +3963,64 @@ var fng;
3097
3963
  find: $location.$$search.f,
3098
3964
  limit: $scope.pageSize,
3099
3965
  skip: pagesLoaded * $scope.pageSize,
3100
- order: $location.$$search.o
3966
+ order: $location.$$search.o,
3967
+ concatenate: false
3101
3968
  })
3102
3969
  .then(function (response) {
3103
3970
  var data = response.data;
3104
3971
  if (angular.isArray(data)) {
3105
- // I have seen an intermittent problem where a page is requested twice
3972
+ // if the options for the resource identified by $scope.modelName has disambiguation parameters,
3973
+ // and that resource has more than one list field, the items returned by getPagedAndFilteredList
3974
+ // might include a "disambiguation" property. for this to appear on the list page, we need
3975
+ // to add an item for it to the list schema
3976
+ if (!$scope.listSchema.find(function (f) { return f.name === "disambiguation"; }) && data.some(function (d) { return d.disambiguation; })) {
3977
+ $scope.listSchema.push({
3978
+ name: "disambiguation",
3979
+ });
3980
+ }
3981
+ // I have seen an intermittent problem where a page is requested twice
3106
3982
  if (pagesLoaded === $scope.pagesLoaded) {
3107
3983
  $scope.pagesLoaded++;
3108
3984
  $scope.recordList = $scope.recordList.concat(data);
3109
3985
  }
3110
3986
  else {
3111
- console.log('DEBUG: infinite scroll component asked for a page twice - the model was ' + $scope.modelName);
3987
+ console.log("DEBUG: infinite scroll component asked for a page twice - the model was " + $scope.modelName);
3112
3988
  }
3113
3989
  }
3114
3990
  else {
3115
- $scope.showError(data, 'Invalid query');
3991
+ $scope.showError(data, "Invalid query");
3116
3992
  }
3117
3993
  }, $scope.handleHttpError);
3118
3994
  },
3119
- // TODO: Do we need model here? Can we not infer it from scope?
3120
- deleteRecord: function deleteRecord(model, id, $scope, ctrlState) {
3121
- SubmissionsService.deleteRecord(model, id)
3995
+ deleteRecord: function deleteRecord(id, $scope, ctrlState) {
3996
+ $scope.phase = "deleting";
3997
+ SubmissionsService.deleteRecord($scope.modelName, id, $scope.formName)
3122
3998
  .then(function () {
3123
- if (typeof $scope.dataEventFunctions.onAfterDelete === 'function') {
3999
+ if (typeof $scope.dataEventFunctions.onAfterDelete === "function") {
3124
4000
  $scope.dataEventFunctions.onAfterDelete(ctrlState.master);
3125
4001
  }
3126
- routingService.redirectTo()('list', $scope, $location);
4002
+ routingService.redirectTo()("onDelete", $scope, $location);
4003
+ }, function (err) {
4004
+ var _a;
4005
+ if (err.status === 404) {
4006
+ // Someone already deleted it
4007
+ routingService.redirectTo()("onDelete", $scope, $location);
4008
+ }
4009
+ else if (err.status === 403) {
4010
+ $scope.showError(((_a = err.data) === null || _a === void 0 ? void 0 : _a.message) || err.message || err.data || err, 'Permission denied');
4011
+ }
4012
+ else {
4013
+ $scope.showError("".concat(err.statusText, " (").concat(err.status, ") while deleting record<br />").concat(err.data), 'Error deleting record');
4014
+ }
3127
4015
  });
3128
4016
  },
3129
4017
  updateDocument: function updateDocument(dataToSave, options, $scope, ctrlState) {
3130
- $scope.phase = 'updating';
3131
- SubmissionsService.updateRecord($scope.modelName, $scope.id, dataToSave)
4018
+ $scope.phase = "updating";
4019
+ SubmissionsService.updateRecord($scope.modelName, $scope.id, dataToSave, $scope.formName)
3132
4020
  .then(function (response) {
3133
4021
  var data = response.data;
3134
4022
  if (data.success !== false) {
3135
- if (typeof $scope.dataEventFunctions.onAfterUpdate === 'function') {
4023
+ if (typeof $scope.dataEventFunctions.onAfterUpdate === "function") {
3136
4024
  $scope.dataEventFunctions.onAfterUpdate(data, ctrlState.master);
3137
4025
  }
3138
4026
  if (options.redirect) {
@@ -3149,21 +4037,24 @@ var fng;
3149
4037
  else {
3150
4038
  $scope.showError(data);
3151
4039
  }
3152
- }, $scope.handleHttpError);
4040
+ }, function (err) {
4041
+ $scope.handleHttpError(err);
4042
+ });
3153
4043
  },
3154
- createNew: function createNew(dataToSave, options, $scope) {
3155
- SubmissionsService.createRecord($scope.modelName, dataToSave)
4044
+ createNew: function createNew(dataToSave, options, $scope, ctrlState) {
4045
+ SubmissionsService.createRecord($scope.modelName, dataToSave, $scope.formName)
3156
4046
  .then(function (response) {
3157
4047
  var data = response.data;
3158
4048
  if (data.success !== false) {
3159
- if (typeof $scope.dataEventFunctions.onAfterCreate === 'function') {
4049
+ ctrlState.allowLocationChange = true;
4050
+ if (typeof $scope.dataEventFunctions.onAfterCreate === "function") {
3160
4051
  $scope.dataEventFunctions.onAfterCreate(data);
3161
4052
  }
3162
4053
  if (options.redirect) {
3163
4054
  $window.location = options.redirect;
3164
4055
  }
3165
4056
  else {
3166
- routingService.redirectTo()('edit', $scope, $location, data._id);
4057
+ routingService.redirectTo()("edit", $scope, $location, data._id);
3167
4058
  }
3168
4059
  }
3169
4060
  else {
@@ -3173,75 +4064,55 @@ var fng;
3173
4064
  },
3174
4065
  getListData: getListData,
3175
4066
  suffixCleanId: suffixCleanId,
4067
+ getData: getData,
3176
4068
  setData: setData,
3177
4069
  setUpLookupOptions: function setUpLookupOptions(lookupCollection, schemaElement, $scope, ctrlState, handleSchema) {
3178
4070
  var optionsList = $scope[schemaElement.options] = [];
3179
4071
  var idList = $scope[schemaElement.ids] = [];
3180
- SchemasService.getSchema(lookupCollection)
4072
+ var dataRequest = !!schemaElement.filter
4073
+ ? SubmissionsService.getPagedAndFilteredList(lookupCollection, Object.assign({ concatenate: true }, schemaElement.filter)) // { concatenate: true } causes it to concatenate the list fields into the .text property of ILookupItem objects
4074
+ : SubmissionsService.getAllListAttributes(lookupCollection);
4075
+ dataRequest
3181
4076
  .then(function (response) {
3182
- var data = response.data;
3183
- var listInstructions = [];
3184
- handleSchema('Lookup ' + lookupCollection, data, null, listInstructions, '', false, $scope, ctrlState);
3185
- var dataRequest;
3186
- if (typeof schemaElement.filter !== 'undefined' && schemaElement.filter) {
3187
- dataRequest = SubmissionsService.getPagedAndFilteredList(lookupCollection, schemaElement.filter);
3188
- }
3189
- else {
3190
- dataRequest = SubmissionsService.getAll(lookupCollection);
3191
- }
3192
- dataRequest
3193
- .then(function (response) {
3194
- var data = response.data;
3195
- if (data) {
3196
- for (var i = 0; i < data.length; i++) {
3197
- var option = '';
3198
- for (var j = 0; j < listInstructions.length; j++) {
3199
- var thisVal = data[i][listInstructions[j].name];
3200
- option += thisVal ? thisVal + ' ' : '';
4077
+ var items = response.data;
4078
+ if (items) {
4079
+ items.sort(function (a, b) { return a.text.localeCompare(b.text); });
4080
+ optionsList.push.apply(optionsList, items.map(function (i) { return i.text; }));
4081
+ idList.push.apply(idList, items.map(function (i) { return i.id; }));
4082
+ var dupes = new Set();
4083
+ for (var i = 0; i < optionsList.length - 1; i++) {
4084
+ for (var j = i + 1; j < optionsList.length; j++) {
4085
+ if (_.isEqual(optionsList[i], optionsList[j])) {
4086
+ dupes.add(optionsList[i]);
3201
4087
  }
3202
- option = option.trim();
3203
- var pos = _.sortedIndex(optionsList, option);
3204
- // handle dupes (ideally people will use unique indexes to stop them but...)
3205
- if (optionsList[pos] === option) {
3206
- option = option + ' (' + data[i]._id + ')';
3207
- pos = _.sortedIndex(optionsList, option);
4088
+ }
4089
+ }
4090
+ // append the id to any duplicates to make them unique
4091
+ dupes.forEach(function (d) {
4092
+ for (var i = 0; i < optionsList.length; i++) {
4093
+ if (optionsList[i] === d) {
4094
+ optionsList[i] += "(" + idList[i] + ")";
3208
4095
  }
3209
- optionsList.splice(pos, 0, option);
3210
- idList.splice(pos, 0, data[i]._id);
3211
4096
  }
3212
- updateRecordWithLookupValues(schemaElement, $scope, ctrlState);
4097
+ });
4098
+ if ($scope.readingRecord) {
4099
+ $scope.readingRecord
4100
+ .then(function () {
4101
+ updateRecordWithLookupValues(schemaElement, $scope, ctrlState);
4102
+ });
3213
4103
  }
3214
- });
4104
+ }
4105
+ })
4106
+ .catch(function (e) {
4107
+ $scope.handleHttpError(e);
3215
4108
  });
3216
4109
  },
3217
4110
  setUpLookupListOptions: function setUpLookupListOptions(ref, formInstructions, $scope, ctrlState) {
3218
4111
  var optionsList = $scope[formInstructions.options] = [];
3219
4112
  var idList = $scope[formInstructions.ids] = [];
3220
- if (ref.id[0] === '$') {
3221
- // id of document we are doing lookup from comes from record, so we need to deal with in $watch
3222
- // by adding it to listLookups
3223
- var nameElements = formInstructions.name.split('.');
3224
- var refHandler = $scope.listLookups.find(function (lkp) {
3225
- return lkp.ref.property === ref.property && lkp.ref.value === ref.value;
3226
- });
3227
- var thisHandler = {
3228
- formInstructions: formInstructions,
3229
- lastPart: nameElements.pop(),
3230
- possibleArray: nameElements.join('.')
3231
- };
3232
- if (!refHandler) {
3233
- refHandler = {
3234
- ref: ref,
3235
- lookupOptions: [],
3236
- lookupIds: [],
3237
- handlers: []
3238
- };
3239
- $scope.listLookups.push(refHandler);
3240
- }
3241
- refHandler.handlers.push(thisHandler);
3242
- $scope[formInstructions.options] = refHandler.lookupOptions;
3243
- $scope[formInstructions.ids] = refHandler.lookupIds;
3244
- // TODO DRY this and handleInternalLookup below
4113
+ if (ref.id[0] === "$") {
4114
+ // id of document that contains out lookup list comes from record, so we need to deal with in $watch by adding it to listLookups
4115
+ addArrayLookupToLookupList($scope, formInstructions, ref, $scope.listLookups);
3245
4116
  }
3246
4117
  else {
3247
4118
  // we can do it now
@@ -3252,7 +4123,7 @@ var fng;
3252
4123
  var pos = _.sortedIndex(optionsList, option);
3253
4124
  // handle dupes
3254
4125
  if (optionsList[pos] === option) {
3255
- option = option + ' (' + data[i]._id + ')';
4126
+ option = option + " (" + data[i]._id + ")";
3256
4127
  pos = _.sortedIndex(optionsList, option);
3257
4128
  }
3258
4129
  optionsList.splice(pos, 0, option);
@@ -3263,27 +4134,7 @@ var fng;
3263
4134
  }
3264
4135
  },
3265
4136
  handleInternalLookup: function handleInternalLookup($scope, formInstructions, ref) {
3266
- var nameElements = formInstructions.name.split('.');
3267
- var refHandler = $scope.internalLookups.find(function (lkp) {
3268
- return lkp.ref.property === ref.property && lkp.ref.value === ref.value;
3269
- });
3270
- var thisHandler = {
3271
- formInstructions: formInstructions,
3272
- lastPart: nameElements.pop(),
3273
- possibleArray: nameElements.join('.')
3274
- };
3275
- if (!refHandler) {
3276
- refHandler = {
3277
- ref: ref,
3278
- lookupOptions: [],
3279
- lookupIds: [],
3280
- handlers: []
3281
- };
3282
- $scope.internalLookups.push(refHandler);
3283
- }
3284
- refHandler.handlers.push(thisHandler);
3285
- $scope[formInstructions.options] = refHandler.lookupOptions;
3286
- $scope[formInstructions.ids] = refHandler.lookupIds;
4137
+ addArrayLookupToLookupList($scope, formInstructions, ref, $scope.internalLookups);
3287
4138
  },
3288
4139
  preservePristine: preservePristine,
3289
4140
  // Reverse the process of convertToAngularModel
@@ -3300,48 +4151,54 @@ var fng;
3300
4151
  }
3301
4152
  return retVal;
3302
4153
  }
3303
- for (var i = 0; i < schema.length; i++) {
3304
- var fieldname = schema[i].name.slice(prefixLength);
3305
- var thisField = getListData($scope, anObject, fieldname);
3306
- if (schema[i].schema) {
4154
+ var _loop_1 = function () {
4155
+ var schemaI = schema[i];
4156
+ var fieldname = schemaI.name.slice(prefixLength);
4157
+ var thisField = getListData(anObject, fieldname, null, $scope);
4158
+ if (schemaI.schema) {
3307
4159
  if (thisField) {
3308
4160
  for (var j = 0; j < thisField.length; j++) {
3309
- thisField[j] = convertToMongoModel(schema[i].schema, thisField[j], prefixLength + 1 + fieldname.length, $scope, fieldname);
4161
+ thisField[j] = convertToMongoModel(schemaI.schema, thisField[j], 1 + fieldname.length, $scope, fieldname);
3310
4162
  }
3311
4163
  }
3312
4164
  }
3313
4165
  else {
3314
4166
  // Convert {array:[{x:'item 1'}]} to {array:['item 1']}
3315
- if (schema[i].array && simpleArrayNeedsX(schema[i]) && thisField) {
4167
+ if (schemaI.array && simpleArrayNeedsX(schemaI) && thisField) {
3316
4168
  for (var k = 0; k < thisField.length; k++) {
3317
4169
  thisField[k] = thisField[k].x;
3318
4170
  }
3319
4171
  }
3320
4172
  // Convert {lookup:'List description for 012abcde'} to {lookup:'012abcde'}
3321
- var idList = $scope[suffixCleanId(schema[i], '_ids')];
3322
- var thisConversion = void 0;
3323
- if (idList && idList.length > 0) {
4173
+ var idList_1 = $scope[suffixCleanId(schemaI, "_ids")];
4174
+ if (idList_1 && idList_1.length > 0) {
3324
4175
  updateObject(fieldname, anObject, function (value) {
3325
- return convertToForeignKeys(schema[i], value, $scope[suffixCleanId(schema[i], 'Options')], idList);
4176
+ return convertToForeignKeys(schemaI, value, $scope[suffixCleanId(schemaI, "Options")], idList_1);
3326
4177
  });
3327
4178
  }
3328
- else if (thisConversion = getConversionObject($scope, fieldname, schemaName)) {
3329
- var lookup = getData(anObject, fieldname, null);
3330
- var newVal;
3331
- if (schema[i].array) {
3332
- newVal = [];
3333
- if (lookup) {
3334
- for (var n = 0; n < lookup.length; n++) {
3335
- newVal[n] = convertLookup(lookup[n], thisConversion);
4179
+ else {
4180
+ var thisConversion = getConversionObject($scope, fieldname, schemaName);
4181
+ if (thisConversion) {
4182
+ var lookup = getData(anObject, fieldname, null);
4183
+ var newVal = void 0;
4184
+ if (schemaI.array) {
4185
+ newVal = [];
4186
+ if (lookup) {
4187
+ for (var n = 0; n < lookup.length; n++) {
4188
+ newVal[n] = convertLookup(lookup[n], thisConversion);
4189
+ }
3336
4190
  }
3337
4191
  }
4192
+ else {
4193
+ newVal = convertLookup(lookup, thisConversion);
4194
+ }
4195
+ setData(anObject, fieldname, null, newVal);
3338
4196
  }
3339
- else {
3340
- newVal = convertLookup(lookup, thisConversion);
3341
- }
3342
- setData(anObject, fieldname, null, newVal);
3343
4197
  }
3344
4198
  }
4199
+ };
4200
+ for (var i = 0; i < schema.length; i++) {
4201
+ _loop_1();
3345
4202
  }
3346
4203
  return anObject;
3347
4204
  },
@@ -3351,7 +4208,7 @@ var fng;
3351
4208
  $scope.handleHttpError = handleError($scope);
3352
4209
  $scope.cancel = function () {
3353
4210
  angular.copy(ctrlState.master, $scope.record);
3354
- $scope.$broadcast('fngCancel', $scope);
4211
+ $scope.$broadcast("fngCancel", $scope);
3355
4212
  // Let call backs etc resolve in case they dirty form, then clean it
3356
4213
  $timeout($scope.setPristine);
3357
4214
  };
@@ -3360,15 +4217,24 @@ var fng;
3360
4217
  // scope.$emit('showErrorMessage', {title: 'Your error Title', body: 'The body of the error message'});
3361
4218
  // or
3362
4219
  // scope.$broadcast('showErrorMessage', {title: 'Your error Title', body: 'The body of the error message'});
3363
- $scope.$on('showErrorMessage', function (event, args) {
3364
- $scope.showError(args.body, args.title);
4220
+ $scope.$on("showErrorMessage", function (event, args) {
4221
+ if (!event.defaultPrevented) {
4222
+ event.defaultPrevented = true;
4223
+ $scope.showError(args.body, args.title);
4224
+ }
3365
4225
  });
3366
4226
  $scope.showError = function (error, alertTitle) {
3367
- $scope.alertTitle = alertTitle ? alertTitle : 'Error!';
3368
- if (typeof error === 'string') {
3369
- $scope.errorMessage = error;
4227
+ $scope.alertTitle = alertTitle ? alertTitle : "Error!";
4228
+ $timeout(function () {
4229
+ $scope.phase = 'ready';
4230
+ }, 25);
4231
+ if (typeof error === "string") {
4232
+ $scope.errorMessage = $sce.trustAsHtml(error);
4233
+ }
4234
+ else if (!error) {
4235
+ $scope.errorMessage = "An error occurred - that's all we got. Sorry.";
3370
4236
  }
3371
- else if (error.message && typeof error.message === 'string') {
4237
+ else if (error.message && typeof error.message === "string") {
3372
4238
  $scope.errorMessage = error.message;
3373
4239
  }
3374
4240
  else if (error.data && error.data.message) {
@@ -3382,16 +4248,35 @@ var fng;
3382
4248
  $scope.errorMessage = error;
3383
4249
  }
3384
4250
  }
4251
+ $scope.errorHideTimer = window.setTimeout(function () {
4252
+ $scope.dismissError();
4253
+ $scope.$digest();
4254
+ }, 3500 + (1000 * ($scope.alertTitle + $scope.errorMessage).length / 50));
4255
+ $scope.errorVisible = true;
4256
+ window.setTimeout(function () {
4257
+ $scope.$digest();
4258
+ });
4259
+ };
4260
+ $scope.clearTimeout = function () {
4261
+ if ($scope.errorHideTimer) {
4262
+ clearTimeout($scope.errorHideTimer);
4263
+ delete $scope.errorHideTimer;
4264
+ }
3385
4265
  };
3386
4266
  $scope.dismissError = function () {
4267
+ $scope.clearTimeout;
4268
+ $scope.errorVisible = false;
3387
4269
  delete $scope.errorMessage;
3388
4270
  delete $scope.alertTitle;
3389
4271
  };
4272
+ $scope.stickError = function () {
4273
+ clearTimeout($scope.errorHideTimer);
4274
+ };
3390
4275
  $scope.prepareForSave = function (cb) {
3391
4276
  //Convert the lookup values into ids
3392
4277
  var dataToSave = recordHandlerInstance.convertToMongoModel($scope.formSchema, angular.copy($scope.record), 0, $scope);
3393
4278
  if ($scope.id) {
3394
- if (typeof $scope.dataEventFunctions.onBeforeUpdate === 'function') {
4279
+ if (typeof $scope.dataEventFunctions.onBeforeUpdate === "function") {
3395
4280
  $scope.dataEventFunctions.onBeforeUpdate(dataToSave, ctrlState.master, function (err) {
3396
4281
  if (err) {
3397
4282
  cb(err);
@@ -3406,7 +4291,7 @@ var fng;
3406
4291
  }
3407
4292
  }
3408
4293
  else {
3409
- if (typeof $scope.dataEventFunctions.onBeforeCreate === 'function') {
4294
+ if (typeof $scope.dataEventFunctions.onBeforeCreate === "function") {
3410
4295
  $scope.dataEventFunctions.onBeforeCreate(dataToSave, function (err) {
3411
4296
  if (err) {
3412
4297
  cb(err);
@@ -3423,24 +4308,29 @@ var fng;
3423
4308
  };
3424
4309
  $scope.save = function (options) {
3425
4310
  options = options || {};
4311
+ // stash these against the scope as well, so the onBeforeUpdate or onBeforeCreate handlers that may be called from
4312
+ // prepareForSave() have knowledge of any redirection that should occur after the save has been successfully made
4313
+ $scope.redirectOptions = options;
3426
4314
  $scope.prepareForSave(function (err, dataToSave) {
3427
4315
  if (err) {
3428
- if (err !== '_update_handled_') {
3429
- $scope.showError(err);
4316
+ if (err !== "_update_handled_") {
4317
+ $timeout(function () {
4318
+ $scope.showError(err);
4319
+ });
3430
4320
  }
3431
4321
  }
3432
4322
  else if ($scope.id) {
3433
4323
  recordHandlerInstance.updateDocument(dataToSave, options, $scope, ctrlState);
3434
4324
  }
3435
4325
  else {
3436
- recordHandlerInstance.createNew(dataToSave, options, $scope);
4326
+ recordHandlerInstance.createNew(dataToSave, options, $scope, ctrlState);
3437
4327
  }
3438
4328
  });
3439
4329
  };
3440
4330
  $scope.newClick = function () {
3441
- routingService.redirectTo()('new', $scope, $location);
4331
+ routingService.redirectTo()("new", $scope, $location);
3442
4332
  };
3443
- $scope.$on('$locationChangeStart', function (event, next) {
4333
+ $scope.$on("$locationChangeStart", function (event, next) {
3444
4334
  // let changed = !$scope.isCancelDisabled();
3445
4335
  // let curPath = window.location.href.split('/');
3446
4336
  // let nextPath = next.split('/');
@@ -3459,21 +4349,12 @@ var fng;
3459
4349
  if (!ctrlState.allowLocationChange && !$scope.isCancelDisabled()) {
3460
4350
  event.preventDefault();
3461
4351
  var modalInstance = $uibModal.open({
3462
- template: '<div class="modal-header">' +
3463
- ' <h3>Record modified</h3>' +
3464
- '</div>' +
3465
- '<div class="modal-body">' +
3466
- ' <p>Would you like to save your changes?</p>' +
3467
- '</div>' +
3468
- '<div class="modal-footer">' +
3469
- ' <button class="btn btn-primary dlg-yes" ng-click="yes()">Yes</button>' +
3470
- ' <button class="btn btn-warning dlg-no" ng-click="no()">No</button>' +
3471
- ' <button class="btn dlg-cancel" ng-click="cancel()">Cancel</button>' +
3472
- '</div>',
3473
- controller: 'SaveChangesModalCtrl',
3474
- backdrop: 'static'
4352
+ template: "<div class=\"modal-header\">\n <h3>Record modified</h3>\n</div>\n<div class=\"modal-body\">\n <p>Would you like to save your changes?</p>\n</div>\n<div class=\"modal-footer\">\n <button class=\"btn btn-primary dlg-yes\" ng-click=\"yes()\">Yes</button>\n <button class=\"btn btn-warning dlg-no\" ng-click=\"no()\">No</button>\n <button class=\"btn dlg-cancel\" ng-click=\"cancel()\">Cancel</button>\n</div>",
4353
+ controller: "SaveChangesModalCtrl",
4354
+ backdrop: "static"
3475
4355
  });
3476
- modalInstance.result.then(function (result) {
4356
+ modalInstance.result
4357
+ .then(function (result) {
3477
4358
  if (result) {
3478
4359
  $scope.save({ redirect: next, allowChange: true }); // save changes
3479
4360
  }
@@ -3481,7 +4362,8 @@ var fng;
3481
4362
  ctrlState.allowLocationChange = true;
3482
4363
  $window.location = next;
3483
4364
  }
3484
- });
4365
+ })
4366
+ .catch(_handleCancel);
3485
4367
  }
3486
4368
  });
3487
4369
  $scope.deleteClick = function () {
@@ -3492,85 +4374,180 @@ var fng;
3492
4374
  }
3493
4375
  else {
3494
4376
  var modalInstance = $uibModal.open({
3495
- template: '<div class="modal-header">' +
3496
- ' <h3>Delete Item</h3>' +
3497
- '</div>' +
3498
- '<div class="modal-body">' +
3499
- ' <p>Are you sure you want to delete this record?</p>' +
3500
- '</div>' +
3501
- '<div class="modal-footer">' +
3502
- ' <button class="btn btn-primary dlg-no" ng-click="cancel()">No</button>' +
3503
- ' <button class="btn btn-warning dlg-yes" ng-click="yes()">Yes</button>' +
3504
- '</div>',
3505
- controller: 'SaveChangesModalCtrl',
3506
- backdrop: 'static'
4377
+ template: "<div class=\"modal-header\">\n <h3>Delete Item</h3>\n</div>\n<div class=\"modal-body\">\n <p>Are you sure you want to delete this record?</p>\n</div>\n<div class=\"modal-footer\">\n <button class=\"btn btn-primary dlg-no\" ng-click=\"cancel()\">No</button>\n <button class=\"btn btn-warning dlg-yes\" ng-click=\"yes()\">Yes</button>\n</div>",
4378
+ controller: "SaveChangesModalCtrl",
4379
+ backdrop: "static"
3507
4380
  });
3508
4381
  confirmDelete = modalInstance.result;
3509
4382
  }
3510
4383
  confirmDelete.then(function (result) {
4384
+ function doTheDeletion() {
4385
+ recordHandlerInstance.deleteRecord($scope.id, $scope, ctrlState);
4386
+ }
3511
4387
  if (result) {
3512
- if (typeof $scope.dataEventFunctions.onBeforeDelete === 'function') {
4388
+ if (typeof $scope.dataEventFunctions.onBeforeDelete === "function") {
3513
4389
  $scope.dataEventFunctions.onBeforeDelete(ctrlState.master, function (err) {
3514
4390
  if (err) {
3515
- if (err !== '_delete_handled_') {
4391
+ if (err !== "_delete_handled_") {
3516
4392
  $scope.showError(err);
3517
4393
  }
3518
4394
  }
3519
4395
  else {
3520
- recordHandlerInstance.deleteRecord($scope.modelName, $scope.id, $scope, ctrlState);
4396
+ doTheDeletion();
3521
4397
  }
3522
4398
  });
3523
4399
  }
3524
4400
  else {
3525
- recordHandlerInstance.deleteRecord($scope.modelName, $scope.id, $scope, ctrlState);
4401
+ doTheDeletion();
3526
4402
  }
3527
4403
  }
3528
- });
4404
+ })
4405
+ .catch(_handleCancel);
3529
4406
  }
3530
4407
  };
3531
4408
  $scope.isCancelDisabled = function () {
3532
- if (typeof $scope.disableFunctions.isCancelDisabled === 'function') {
4409
+ var _a;
4410
+ if (($scope[$scope.topLevelFormName] && $scope[$scope.topLevelFormName].$pristine) || $scope.phase !== "ready") {
4411
+ return true;
4412
+ }
4413
+ else if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isCancelDisabled) === "function") {
3533
4414
  return $scope.disableFunctions.isCancelDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
3534
4415
  }
3535
4416
  else {
3536
- return $scope[$scope.topLevelFormName] && $scope[$scope.topLevelFormName].$pristine;
4417
+ return false;
3537
4418
  }
3538
4419
  };
3539
4420
  $scope.isSaveDisabled = function () {
3540
- if (typeof $scope.disableFunctions.isSaveDisabled === 'function') {
3541
- return $scope.disableFunctions.isSaveDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
4421
+ var _a;
4422
+ $scope.whyDisabled = undefined;
4423
+ var pristine = false;
4424
+ function generateWhyDisabledMessage(form, subFormName) {
4425
+ form.$$controls.forEach(function (c) {
4426
+ if (c.$invalid) {
4427
+ if (c.$$controls) {
4428
+ // nested form
4429
+ generateWhyDisabledMessage(c, c.$name);
4430
+ }
4431
+ else {
4432
+ $scope.whyDisabled += "<br /><strong>";
4433
+ if (subFormName) {
4434
+ $scope.whyDisabled += subFormName + ' ';
4435
+ }
4436
+ if (cssFrameworkService.framework() === "bs2" &&
4437
+ c.$$element &&
4438
+ c.$$element.parent() &&
4439
+ c.$$element.parent().parent() &&
4440
+ c.$$element.parent().parent().find("label") &&
4441
+ c.$$element.parent().parent().find("label").text()) {
4442
+ $scope.whyDisabled += c.$$element.parent().parent().find("label").text();
4443
+ }
4444
+ else if (cssFrameworkService.framework() === "bs3" &&
4445
+ c.$$element &&
4446
+ c.$$element.parent() &&
4447
+ c.$$element.parent().parent() &&
4448
+ c.$$element.parent().parent().parent() &&
4449
+ c.$$element.parent().parent().parent().find("label") &&
4450
+ c.$$element.parent().parent().parent().find("label").text()) {
4451
+ $scope.whyDisabled += c.$$element.parent().parent().parent().find("label").text();
4452
+ }
4453
+ else {
4454
+ $scope.whyDisabled += c.$name;
4455
+ }
4456
+ $scope.whyDisabled += "</strong>: ";
4457
+ if (c.$error) {
4458
+ for (var type in c.$error) {
4459
+ if (c.$error.hasOwnProperty(type)) {
4460
+ switch (type) {
4461
+ case "required":
4462
+ $scope.whyDisabled += "Field missing required value. ";
4463
+ break;
4464
+ case "pattern":
4465
+ $scope.whyDisabled += "Field does not match required pattern. ";
4466
+ break;
4467
+ default:
4468
+ $scope.whyDisabled += type + ". ";
4469
+ }
4470
+ }
4471
+ }
4472
+ }
4473
+ }
4474
+ }
4475
+ });
4476
+ }
4477
+ if ($scope[$scope.topLevelFormName]) {
4478
+ if ($scope[$scope.topLevelFormName].$invalid) {
4479
+ $scope.whyDisabled = 'The form data is invalid:';
4480
+ generateWhyDisabledMessage($scope[$scope.topLevelFormName]);
4481
+ }
4482
+ else if ($scope[$scope.topLevelFormName].$pristine) {
4483
+ // Don't have disabled message - should be obvious from Cancel being disabled,
4484
+ // and the message comes up when the Save button is clicked.
4485
+ pristine = true;
4486
+ }
3542
4487
  }
3543
4488
  else {
3544
- return ($scope[$scope.topLevelFormName] && ($scope[$scope.topLevelFormName].$invalid || $scope[$scope.topLevelFormName].$pristine));
4489
+ $scope.whyDisabled = "Top level form name invalid";
4490
+ }
4491
+ if (pristine || !!$scope.whyDisabled || $scope.phase !== "ready") {
4492
+ return true;
4493
+ }
4494
+ else if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isSaveDisabled) !== "function") {
4495
+ return false;
4496
+ }
4497
+ else {
4498
+ var retVal = $scope.disableFunctions.isSaveDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
4499
+ if (typeof retVal === "string") {
4500
+ $scope.whyDisabled = retVal;
4501
+ }
4502
+ else {
4503
+ $scope.whyDisabled = "An application level user-specified function is inhibiting saving the record";
4504
+ }
4505
+ return !!retVal;
3545
4506
  }
3546
4507
  };
3547
4508
  $scope.isDeleteDisabled = function () {
3548
- if (typeof $scope.disableFunctions.isDeleteDisabled === 'function') {
4509
+ var _a;
4510
+ if (!$scope.id || $scope.phase !== "ready") {
4511
+ return true;
4512
+ }
4513
+ else if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isDeleteDisabled) === "function") {
3549
4514
  return $scope.disableFunctions.isDeleteDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
3550
4515
  }
3551
4516
  else {
3552
- return (!$scope.id);
4517
+ return false;
3553
4518
  }
3554
4519
  };
3555
4520
  $scope.isNewDisabled = function () {
3556
- if (typeof $scope.disableFunctions.isNewDisabled === 'function') {
4521
+ var _a;
4522
+ if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isNewDisabled) === "function") {
3557
4523
  return $scope.disableFunctions.isNewDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
3558
4524
  }
3559
4525
  else {
3560
4526
  return false;
3561
4527
  }
3562
4528
  };
3563
- $scope.disabledText = function (localStyling) {
3564
- var text = '';
3565
- if ($scope.isSaveDisabled) {
3566
- text = 'This button is only enabled when the form is complete and valid. Make sure all required inputs are filled in. ' + localStyling;
3567
- }
3568
- return text;
4529
+ $scope.setDefaults = function (formSchema, base) {
4530
+ if (base === void 0) { base = ''; }
4531
+ var retVal = {};
4532
+ formSchema.forEach(function (s) {
4533
+ if (s.defaultValue !== undefined) {
4534
+ var nameParts = s.name.replace(base, '').split(".");
4535
+ var target = retVal;
4536
+ for (var i = 0; i < nameParts.length - 1; i++) {
4537
+ if (!target[nameParts[i]]) {
4538
+ target[nameParts[i]] = {};
4539
+ }
4540
+ target = target[nameParts[i]];
4541
+ }
4542
+ target[nameParts[nameParts.length - 1]] = s.defaultValue;
4543
+ }
4544
+ });
4545
+ return retVal;
3569
4546
  };
3570
4547
  $scope.getVal = function (expression, index) {
3571
- if (expression.indexOf('$index') === -1 || typeof index !== 'undefined') {
4548
+ if (expression.indexOf("$index") === -1 || typeof index !== "undefined") {
3572
4549
  expression = expression.replace(/\$index/g, index);
3573
- return $scope.$eval('record.' + expression);
4550
+ return $scope.$eval("record." + expression);
3574
4551
  }
3575
4552
  //else {
3576
4553
  // Used to show error here, but angular seems to call before record is populated sometimes
@@ -3578,11 +4555,42 @@ var fng;
3578
4555
  //}
3579
4556
  };
3580
4557
  $scope.sortableOptions = {
3581
- update: function () {
3582
- if ($scope.topLevelFormName) {
4558
+ update: function (e, ui) {
4559
+ if (e.target.hasAttribute("disabled")) {
4560
+ // where formsAngular.elemSecurityFuncBinding is set to "one-time" or "normal", the <ol> that the
4561
+ // ui-sortable directive has been used with will have an ng-disabled that may or may not have caused
4562
+ // a disabled attribute to be added to that element. in the case where this attribute has been
4563
+ // added, sorting should be prevented.
4564
+ // allowing the user to begin the drag, and then preventing it only once they release the mouse button,
4565
+ // doesn't seem like the best solution, but I've yet to find something that works better. the
4566
+ // cancel property (see commented-out code below) looks like it should work (and kind of does), but this
4567
+ // screws up mouse events on input fields hosted within the draggable <li> items, so you're
4568
+ // basically prevented from updating any form element in the nested schema
4569
+ ui.item.sortable.cancel();
4570
+ }
4571
+ else if ($scope.topLevelFormName) {
3583
4572
  $scope[$scope.topLevelFormName].$setDirty();
3584
4573
  }
4574
+ },
4575
+ // don't do this (see comment above)
4576
+ //cancel: "ol[disabled]>li"
4577
+ };
4578
+ $scope.setUpCustomLookupOptions = function (schemaElement, ids, options, baseScope) {
4579
+ for (var _i = 0, _a = [$scope, baseScope]; _i < _a.length; _i++) {
4580
+ var scope = _a[_i];
4581
+ if (scope) {
4582
+ // need to be accessible on our scope for generation of the select options, and - for nested schemas -
4583
+ // on baseScope for the conversion back to ids done by prepareForSave
4584
+ scope[schemaElement.ids] = ids;
4585
+ scope[schemaElement.options] = options;
4586
+ }
4587
+ }
4588
+ var data = getData($scope.record, schemaElement.name);
4589
+ if (!data) {
4590
+ return;
3585
4591
  }
4592
+ data = convertForeignKeys(schemaElement, data, options, ids);
4593
+ setData($scope.record, schemaElement.name, undefined, data);
3586
4594
  };
3587
4595
  },
3588
4596
  fillFormFromBackendCustomSchema: fillFormFromBackendCustomSchema,
@@ -3621,17 +4629,267 @@ var fng;
3621
4629
  var services;
3622
4630
  (function (services) {
3623
4631
  /*@ngInject*/
3624
- SubmissionsService.$inject = ["$http", "$cacheFactory"];
3625
- function SubmissionsService($http, $cacheFactory) {
4632
+ securityService.$inject = ["$rootScope"];
4633
+ function securityService($rootScope) {
4634
+ function canDoSecurity(type) {
4635
+ return (!!fng.formsAngular.elemSecurityFuncBinding &&
4636
+ ((type === "hidden" && !!fng.formsAngular.hiddenSecurityFuncName) ||
4637
+ (type === "disabled" && !!fng.formsAngular.disabledSecurityFuncName)));
4638
+ }
4639
+ function canDoSecurityNow(scope, type) {
4640
+ return (canDoSecurity(type) && // we have security configured
4641
+ (
4642
+ // the host app has not (temporarily) disabled this security type (which it might do, as an optimisation, when there are
4643
+ // currently no security rules to apply); and
4644
+ // it has provided the callbacks that are specified in the security configuration; and
4645
+ // the provided scope (if any) has been decorated (by us). pages and popups which aren't form controllers will need to use
4646
+ // (either directly, or through formMarkupHelper), the decorateSecurableScope() function below
4647
+ (type === "hidden" &&
4648
+ $rootScope[fng.formsAngular.hiddenSecurityFuncName] &&
4649
+ (!scope || !!scope.isSecurelyHidden))
4650
+ ||
4651
+ (type === "disabled" &&
4652
+ $rootScope[fng.formsAngular.disabledSecurityFuncName] &&
4653
+ (!scope || !!scope.isSecurelyDisabled))));
4654
+ }
4655
+ function isSecurelyHidden(elemId, pseudoUrl) {
4656
+ return $rootScope[fng.formsAngular.hiddenSecurityFuncName](elemId, pseudoUrl);
4657
+ }
4658
+ function getSecureDisabledState(elemId, pseudoUrl) {
4659
+ return $rootScope[fng.formsAngular.disabledSecurityFuncName](elemId, pseudoUrl);
4660
+ }
4661
+ function isSecurelyDisabled(elemId, pseudoUrl) {
4662
+ return !!getSecureDisabledState(elemId, pseudoUrl); // either true or "+"
4663
+ }
4664
+ function getBindingStr() {
4665
+ return fng.formsAngular.elemSecurityFuncBinding === "one-time" ? "::" : "";
4666
+ }
4667
+ function ignoreElemId(elemId) {
4668
+ var _a;
4669
+ return (_a = fng.formsAngular.ignoreIdsForHideableOrDisableableAttrs) === null || _a === void 0 ? void 0 : _a.some(function (id) { return elemId.includes(id); });
4670
+ }
4671
+ function getXableAttrs(elemId, attr) {
4672
+ if (elemId && attr && !ignoreElemId(elemId)) {
4673
+ return " ".concat(attr, " title=\"").concat(elemId, "\"");
4674
+ }
4675
+ else {
4676
+ return "";
4677
+ }
4678
+ }
4679
+ function getDisableableAttrs(elemId) {
4680
+ // even when an element should not actually be disabled, we should still mark what would otherwise have been a
4681
+ // potentially-disabled element with scope.disableableAttr - where this is set - and where it is set, also set its
4682
+ // title to be the same as its id so that users can learn of its id by hovering over it. this will
4683
+ // help anyone trying to figure out what is the right element id to use for a DOM security rule
4684
+ return getXableAttrs(elemId, fng.formsAngular.disableableAttr);
4685
+ }
4686
+ function getDisableableAncestorAttrs(elemId) {
4687
+ // even when an element should not actually be disabled, we should still mark what would otherwise have been a
4688
+ // potentially-disabled element with scope.disableableAttr - where this is set - and where it is set, also set its
4689
+ // title to be the same as its id so that users can learn of its id by hovering over it. this will
4690
+ // help anyone trying to figure out what is the right element id to use for a DOM security rule
4691
+ return getXableAttrs(elemId, fng.formsAngular.disableableAncestorAttr);
4692
+ }
4693
+ function getHideableAttrs(elemId) {
4694
+ // even when canDoSecurityNow() returns false, we should still mark what would otherwise have been a
4695
+ // potentially-hidden element with scope.hideableAttr, where this is set, and where it is set, also set its
4696
+ // title to be the same as its id so that users can learn of its id by hovering over it. this will
4697
+ // help anyone trying to figure out what is the right element id to use for a DOM security rule
4698
+ return getXableAttrs(elemId, fng.formsAngular.hideableAttr);
4699
+ }
4700
+ return {
4701
+ canDoSecurity: canDoSecurity,
4702
+ canDoSecurityNow: canDoSecurityNow,
4703
+ isSecurelyHidden: isSecurelyHidden,
4704
+ isSecurelyDisabled: isSecurelyDisabled,
4705
+ getHideableAttrs: getHideableAttrs,
4706
+ getDisableableAttrs: getDisableableAttrs,
4707
+ getDisableableAncestorAttrs: getDisableableAncestorAttrs,
4708
+ // whilst initialising new pages and popups, pass their scope here for decoration with functions that can be used to check
4709
+ // the disabled / hidden state of DOM elements on that page according to the prevailing security rules.
4710
+ // if the host app indicates that security checks should be skipped for this page, we will NOT assign the corresponding
4711
+ // functions. the presence of these functions will be checked later by canDoSecurityNow(), which will always be called
4712
+ // before any security logic is applied. this allows security to be bypassed entirely at the request of the host app,
4713
+ // providing an opportunity for optimisation.
4714
+ decorateSecurableScope: function (securableScope, params) {
4715
+ if (canDoSecurity("hidden") && (!fng.formsAngular.skipHiddenSecurityFuncName || (params === null || params === void 0 ? void 0 : params.overrideSkipping) || typeof $rootScope[fng.formsAngular.skipHiddenSecurityFuncName] !== "function" || !$rootScope[fng.formsAngular.skipHiddenSecurityFuncName](params === null || params === void 0 ? void 0 : params.pseudoUrl))) {
4716
+ securableScope.isSecurelyHidden = function (elemId) {
4717
+ return isSecurelyHidden(elemId, params === null || params === void 0 ? void 0 : params.pseudoUrl);
4718
+ };
4719
+ }
4720
+ if (canDoSecurity("disabled") && (!fng.formsAngular.skipDisabledSecurityFuncName || (params === null || params === void 0 ? void 0 : params.overrideSkipping) || typeof $rootScope[fng.formsAngular.skipDisabledSecurityFuncName] !== "function" || !$rootScope[fng.formsAngular.skipDisabledSecurityFuncName](params === null || params === void 0 ? void 0 : params.pseudoUrl))) {
4721
+ securableScope.isSecurelyDisabled = function (elemId) {
4722
+ return isSecurelyDisabled(elemId, params === null || params === void 0 ? void 0 : params.pseudoUrl);
4723
+ };
4724
+ if (!fng.formsAngular.skipDisabledAncestorSecurityFuncName || (params === null || params === void 0 ? void 0 : params.overrideSkipping) || typeof $rootScope[fng.formsAngular.skipDisabledAncestorSecurityFuncName] || !$rootScope[fng.formsAngular.skipDisabledAncestorSecurityFuncName](params === null || params === void 0 ? void 0 : params.pseudoUrl)) {
4725
+ securableScope.requiresDisabledChildren = function (elemId) {
4726
+ return getSecureDisabledState(elemId, params === null || params === void 0 ? void 0 : params.pseudoUrl) === "+";
4727
+ };
4728
+ }
4729
+ }
4730
+ },
4731
+ doSecurityWhenReady: function (cb) {
4732
+ if (canDoSecurityNow(undefined, "hidden")) {
4733
+ cb();
4734
+ }
4735
+ else if (canDoSecurity("hidden")) {
4736
+ // wait until the hidden security function has been provided (externally) before proceeding with the callback...
4737
+ // we assume here that the hidden security and disabled security functions are both going to be provided at the
4738
+ // same time (and could therefore watch for either of these things)
4739
+ var unwatch_2 = $rootScope.$watch(fng.formsAngular.hiddenSecurityFuncName, function (newValue) {
4740
+ if (newValue) {
4741
+ unwatch_2();
4742
+ cb();
4743
+ }
4744
+ });
4745
+ }
4746
+ },
4747
+ considerVisibility: function (id, scope) {
4748
+ var hideableAttrs = getHideableAttrs(id);
4749
+ if (canDoSecurityNow(scope, "hidden")) {
4750
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
4751
+ if (scope.isSecurelyHidden(id)) {
4752
+ // if our securityFunc supports instant binding and evaluates to true, then nothing needs to be
4753
+ // added to the dom for this field, which we indicate to our caller as follows...
4754
+ return { omit: true };
4755
+ }
4756
+ }
4757
+ else {
4758
+ return { visibilityAttr: "data-ng-if=\"".concat(getBindingStr(), "!isSecurelyHidden('").concat(id, "')\"").concat(hideableAttrs) };
4759
+ }
4760
+ }
4761
+ return { visibilityAttr: hideableAttrs };
4762
+ },
4763
+ // consider the visibility of a container whose visibility depends upon at least some of its content being visible.
4764
+ // the container is assumed not to itself be securable (hence it doesn't have an id - or at least, not one we're
4765
+ // concerned about - and it doesn't itself need a "hideable" attribute)
4766
+ considerContainerVisibility: function (contentIds, scope) {
4767
+ if (canDoSecurityNow(scope, "hidden")) {
4768
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
4769
+ if (contentIds.some(function (id) { return !scope.isSecurelyHidden(id); })) {
4770
+ return {};
4771
+ }
4772
+ else {
4773
+ return { omit: true };
4774
+ }
4775
+ }
4776
+ else {
4777
+ var attrs = contentIds.map(function (id) { return "!isSecurelyHidden('".concat(id, "')"); });
4778
+ return { visibilityAttr: "data-ng-if=\"".concat(getBindingStr(), "(").concat(attrs.join(" || "), ")\"") };
4779
+ }
4780
+ }
4781
+ return {};
4782
+ },
4783
+ // Generate an attribute that could be added to the element with the given id to enable or disable it
4784
+ // according to the prevailing security rules. In most cases, the attribute will either be "disabled"
4785
+ // (in the case of 'instant' binding) or data-ng-disabled="xxxx" (in the case of one-time or normal
4786
+ // binding). For directives that require something different, use params:
4787
+ // - forceNg will wrap a positive (disabled) result in an angular directive (e.g., data-ng-disabled="true")
4788
+ // rather than returning simply "disabled"
4789
+ // - attrRequiresValue will translate a positive (disabled) result into an attribute with a truthy value
4790
+ // (e.g., disabled="true") rather than returning simply "disabled"
4791
+ // - attr can be used in the case where a directive expects an attribute other than "disabled".
4792
+ // (for example, uib-tab expects "disable").
4793
+ // Even if the element is not be disabled on this occasion, we will always return the value of
4794
+ // fng.formsAngular.disableableAttr - where set - as a way of marking it as potentially disableable.
4795
+ // Because they can also have a readonly attribute which needs to be taken into consideration, this
4796
+ // function is NOT suitable for fields, which are instead handled by fieldformMarkupHelper.handleReadOnlyDisabled().
4797
+ generateDisabledAttr: function (id, scope, params) {
4798
+ function getActuallyDisabledAttrs() {
4799
+ var result = "";
4800
+ if (canDoSecurityNow(scope, "disabled")) {
4801
+ if (!params) {
4802
+ params = {};
4803
+ }
4804
+ if (params.attrRequiresValue && params.forceNg) {
4805
+ throw new Error("Invalid combination of parameters provided to generateDisabledAttr() [attrRequiresValue and forceNg]");
4806
+ }
4807
+ var attr = params.attr || "disabled";
4808
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
4809
+ if (scope.isSecurelyDisabled(id)) {
4810
+ if (params.attrRequiresValue) {
4811
+ return " ".concat(attr, "=\"true\"");
4812
+ }
4813
+ else if (params.forceNg) {
4814
+ result = "true";
4815
+ }
4816
+ else {
4817
+ return " ".concat(attr);
4818
+ }
4819
+ }
4820
+ }
4821
+ else {
4822
+ result = "".concat(getBindingStr(), "isSecurelyDisabled('").concat(id, "')");
4823
+ }
4824
+ if (result) {
4825
+ if (attr === "disabled") {
4826
+ return " data-ng-disabled=\"".concat(result, "\"");
4827
+ }
4828
+ else {
4829
+ return " data-ng-attr-".concat(attr, "=\"").concat(result, "\"");
4830
+ }
4831
+ }
4832
+ }
4833
+ return result;
4834
+ }
4835
+ return getActuallyDisabledAttrs() + getDisableableAttrs(id);
4836
+ },
4837
+ };
4838
+ }
4839
+ services.securityService = securityService;
4840
+ })(services = fng.services || (fng.services = {}));
4841
+ })(fng || (fng = {}));
4842
+ /// <reference path="../../../../node_modules/@types/angular/index.d.ts" />
4843
+ var ExpirationCache = /** @class */ (function () {
4844
+ function ExpirationCache(timeout) {
4845
+ if (timeout === void 0) { timeout = 60 * 1000; }
4846
+ this.store = new Map();
4847
+ this.timeout = timeout;
4848
+ }
4849
+ ExpirationCache.prototype.get = function (key) {
4850
+ // this.store.has(key) ? console.log(`cache hit`) : console.log(`cache miss`);
4851
+ return this.store.get(key);
4852
+ };
4853
+ ExpirationCache.prototype.put = function (key, val) {
4854
+ var _this = this;
4855
+ this.store.set(key, val);
4856
+ // remove it once it's expired
4857
+ setTimeout(function () {
4858
+ // console.log(`removing expired key ${key}`);
4859
+ _this.remove(key);
4860
+ }, this.timeout);
4861
+ };
4862
+ ExpirationCache.prototype.remove = function (key) {
4863
+ this.store.delete(key);
4864
+ };
4865
+ ExpirationCache.prototype.removeAll = function () {
4866
+ this.store = new Map();
4867
+ };
4868
+ ExpirationCache.prototype.delete = function () {
4869
+ //no op here because this is standalone, not a part of $cacheFactory
4870
+ };
4871
+ return ExpirationCache;
4872
+ }());
4873
+ var fng;
4874
+ (function (fng) {
4875
+ var services;
4876
+ (function (services) {
4877
+ /*@ngInject*/
4878
+ SubmissionsService.$inject = ["$http"];
4879
+ function SubmissionsService($http) {
4880
+ var useCacheForGetAll = true;
4881
+ var expCache = new ExpirationCache();
3626
4882
  /*
3627
4883
  generate a query string for a filtered and paginated query for submissions.
3628
4884
  options consists of the following:
3629
4885
  {
3630
4886
  aggregate - whether or not to aggregate results (http://docs.mongodb.org/manual/aggregation/)
3631
4887
  find - find parameter
4888
+ projection - the fields to return
3632
4889
  limit - limit results to this number of records
3633
4890
  skip - skip this number of records before returning results
3634
4891
  order - sort order
4892
+ concatenate - whether to concatenate all of the list fields into a single text field (and return { id, text }[]), or not (in which case the documents - albeit only list fields and _id - are returned without transformation)
3635
4893
  }
3636
4894
  */
3637
4895
  var generateListQuery = function (options) {
@@ -3652,11 +4910,23 @@ var fng;
3652
4910
  };
3653
4911
  addParameter('l', options.limit);
3654
4912
  addParameter('f', options.find);
4913
+ addParameter('p', options.projection);
3655
4914
  addParameter('a', options.aggregate);
3656
4915
  addParameter('o', options.order);
3657
4916
  addParameter('s', options.skip);
4917
+ addParameter('c', options.concatenate);
3658
4918
  return queryString;
3659
4919
  };
4920
+ function generateUrl(modelName, formName, id) {
4921
+ var url = "/api/".concat(modelName);
4922
+ if (formName) {
4923
+ url += "/".concat(formName);
4924
+ }
4925
+ if (id) {
4926
+ url += "/".concat(id);
4927
+ }
4928
+ return url;
4929
+ }
3660
4930
  // TODO Figure out tab history updates (check for other tab-history-todos)
3661
4931
  //
3662
4932
  // interface ITabChange {
@@ -3679,20 +4949,48 @@ var fng;
3679
4949
  // changed: changed
3680
4950
  // };
3681
4951
  // },
3682
- getListAttributes: function (ref, id) {
3683
- if (typeof ref === 'string') {
3684
- ref = { type: 'lookup', collection: ref };
3685
- console.warn("Support for string type \"ref\" property is deprecated - use ref:" + JSON.stringify(ref));
4952
+ // return only the list attributes for the given record. where returnRaw is true, the record's
4953
+ // list attributes will be returned without transformation. otherwise, the list attributes will be concatenated
4954
+ // (with spaces) and returned in the form { list: string }
4955
+ getListAttributes: function (ref, id, returnRaw) {
4956
+ var actualId = typeof id === "string" ? id : id.id || id._id || id.x || id;
4957
+ if (typeof actualId === "object") {
4958
+ throw new Error("getListAttributes doesn't expect an object but was provided with ".concat(JSON.stringify(id)));
4959
+ }
4960
+ var queryString = returnRaw ? "?returnRaw=1" : "";
4961
+ return $http.get("/api/".concat(ref, "/").concat(actualId, "/list").concat(queryString), { cache: expCache });
4962
+ },
4963
+ // return only the list attributes for ALL records in the given collection, returning ILookupItem[]
4964
+ getAllListAttributes: function (ref) {
4965
+ return $http.get("/api/".concat(ref, "/listAll"), { cache: expCache });
4966
+ },
4967
+ // return only the list attributes for records in the given collection that satisfy the given query conditions (filter, limit etc.)
4968
+ // return ILookupItem[] if options.concatenate is true, else the raw documents
4969
+ getPagedAndFilteredList: function (ref, options) {
4970
+ if (options.projection) {
4971
+ throw new Error("Cannot use projection option for getPagedAndFilteredList, because it only returns list fields");
4972
+ }
4973
+ if (options.concatenate === undefined) {
4974
+ options.concatenate = false;
3686
4975
  }
3687
- return $http.get('/api/' + ref.collection + '/' + id + '/list');
4976
+ return $http.get("/api/".concat(ref, "/listAll").concat(generateListQuery(options)));
3688
4977
  },
3689
- readRecord: function (modelName, id) {
4978
+ // return ALL attributes for records in the given collection that satisfy the given query conditions (filter, limit etc.)
4979
+ getPagedAndFilteredListFull: function (ref, options) {
4980
+ return $http.get("/api/".concat(ref).concat(generateListQuery(options)));
4981
+ },
4982
+ readRecord: function (modelName, id, formName) {
3690
4983
  // TODO Figure out tab history updates (check for other tab-history-todos)
3691
4984
  // let retVal;
3692
4985
  // if (tabChangeData && tabChangeData.model === modelName && tabChangeData.id === id) {
3693
4986
  // retVal = Promise.resolve({data:tabChangeData.record, changed: tabChangeData.changed, master: tabChangeData.master});
3694
4987
  // } else {
3695
- return $http.get('/api/' + modelName + '/' + id);
4988
+ var actualId = typeof id === "string" ? id : id.id || id._id || id.x || id;
4989
+ if (typeof actualId === "object") {
4990
+ throw new Error("readRecord doesn't expect an object but was provided with ".concat(JSON.stringify(id)));
4991
+ }
4992
+ var url = generateUrl(modelName, formName, actualId);
4993
+ return $http.get(url);
3696
4994
  // retVal = $http.get('/api/' + modelName + '/' + id);
3697
4995
  // }
3698
4996
  // tabChangeData = null;
@@ -3700,23 +4998,32 @@ var fng;
3700
4998
  },
3701
4999
  getAll: function (modelName, _options) {
3702
5000
  var options = angular.extend({
3703
- cache: true
5001
+ cache: useCacheForGetAll ? expCache : false
3704
5002
  }, _options);
3705
- return $http.get('/api/' + modelName, options);
5003
+ return $http.get("/api/".concat(modelName), options);
3706
5004
  },
3707
- getPagedAndFilteredList: function (modelName, options) {
3708
- return $http.get('/api/' + modelName + generateListQuery(options));
5005
+ deleteRecord: function (modelName, id, formName) {
5006
+ var url = generateUrl(modelName, formName, id);
5007
+ return $http.delete(url);
3709
5008
  },
3710
- deleteRecord: function (model, id) {
3711
- return $http.delete('/api/' + model + '/' + id);
5009
+ updateRecord: function (modelName, id, dataToSave, formName) {
5010
+ expCache.remove("/api/".concat(modelName));
5011
+ var url = generateUrl(modelName, formName, id);
5012
+ return $http.post(url, dataToSave);
3712
5013
  },
3713
- updateRecord: function (modelName, id, dataToSave) {
3714
- $cacheFactory.get('$http').remove('/api/' + modelName);
3715
- return $http.post('/api/' + modelName + '/' + id, dataToSave);
5014
+ createRecord: function (modelName, dataToSave, formName) {
5015
+ expCache.remove("/api/".concat(modelName));
5016
+ var url = generateUrl(modelName, formName) + "/new";
5017
+ return $http.post(url, dataToSave);
3716
5018
  },
3717
- createRecord: function (modelName, dataToSave) {
3718
- $cacheFactory.get('$http').remove('/api/' + modelName);
3719
- return $http.post('/api/' + modelName, dataToSave);
5019
+ useCache: function (val) {
5020
+ useCacheForGetAll = val;
5021
+ },
5022
+ getCache: function () {
5023
+ return !!expCache;
5024
+ },
5025
+ clearCache: function () {
5026
+ expCache.removeAll();
3720
5027
  }
3721
5028
  };
3722
5029
  }
@@ -3742,6 +5049,7 @@ var fng;
3742
5049
  fngInvalidRequired: 'fng-invalid-required',
3743
5050
  allowLocationChange: true // Set when the data arrives..
3744
5051
  };
5052
+ $scope.errorVisible = false;
3745
5053
  angular.extend($scope, routingService.parsePathFunc()($location.$$path));
3746
5054
  // Load context menu. For /person/client/:id/edit we need
3747
5055
  // to load PersonCtrl and PersonClientCtrl
@@ -3756,17 +5064,33 @@ var fng;
3756
5064
  $rootScope.$broadcast('fngFormLoadStart', $scope);
3757
5065
  formGenerator.decorateScope($scope, formGenerator, recordHandler, $scope.sharedData);
3758
5066
  recordHandler.decorateScope($scope, $uibModal, recordHandler, ctrlState);
3759
- recordHandler.fillFormWithBackendSchema($scope, formGenerator, recordHandler, ctrlState);
3760
- // Tell the 'model controllers' that they can start fiddling with basescope
3761
- for (var i = 0; i < $scope.sharedData.modelControllers.length; i++) {
3762
- if ($scope.sharedData.modelControllers[i].onBaseCtrlReady) {
3763
- $scope.sharedData.modelControllers[i].onBaseCtrlReady($scope);
5067
+ function processTheForm() {
5068
+ recordHandler.fillFormWithBackendSchema($scope, formGenerator, recordHandler, ctrlState);
5069
+ // Tell the 'model controllers' that they can start fiddling with baseScope
5070
+ for (var i = 0; i < $scope.sharedData.modelControllers.length; i++) {
5071
+ if ($scope.sharedData.modelControllers[i].onBaseCtrlReady) {
5072
+ $scope.sharedData.modelControllers[i].onBaseCtrlReady($scope);
5073
+ }
3764
5074
  }
5075
+ $scope.$on('$destroy', function () {
5076
+ $scope.sharedData.modelControllers.forEach(function (value) { return value.$destroy(); });
5077
+ $rootScope.$broadcast('fngControllersUnloaded');
5078
+ });
5079
+ }
5080
+ //Check that we are ready
5081
+ if (typeof fng.formsAngular.beforeProcess === "function") {
5082
+ fng.formsAngular.beforeProcess($scope, function (err) {
5083
+ if (err) {
5084
+ $scope.showError(err.message, 'Error preparing to process form');
5085
+ }
5086
+ else {
5087
+ processTheForm();
5088
+ }
5089
+ });
5090
+ }
5091
+ else {
5092
+ processTheForm();
3765
5093
  }
3766
- $scope.$on('$destroy', function () {
3767
- $scope.sharedData.modelControllers.forEach(function (value) { return value.$destroy(); });
3768
- $rootScope.$broadcast('fngControllersUnloaded');
3769
- });
3770
5094
  }
3771
5095
  controllers.BaseCtrl = BaseCtrl;
3772
5096
  })(controllers = fng.controllers || (fng.controllers = {}));
@@ -3822,13 +5146,20 @@ var fng;
3822
5146
  var controllers;
3823
5147
  (function (controllers) {
3824
5148
  /*@ngInject*/
3825
- NavCtrl.$inject = ["$scope", "$location", "$filter", "routingService", "cssFrameworkService"];
3826
- function NavCtrl($scope, $location, $filter, routingService, cssFrameworkService) {
5149
+ NavCtrl.$inject = ["$rootScope", "$window", "$scope", "$filter", "routingService", "cssFrameworkService", "securityService"];
5150
+ function NavCtrl($rootScope, $window, $scope, $filter, routingService, cssFrameworkService, securityService) {
3827
5151
  function clearContextMenu() {
3828
5152
  $scope.items = [];
3829
5153
  $scope.contextMenu = undefined;
5154
+ $scope.contextMenuId = undefined;
5155
+ $scope.contextMenuHidden = undefined;
5156
+ $scope.contextMenuDisabled = undefined;
3830
5157
  }
5158
+ $rootScope.navScope = $scope; // Lets plugins access menus
3831
5159
  clearContextMenu();
5160
+ $scope.toggleCollapsed = function () {
5161
+ $scope.collapsed = !$scope.collapsed;
5162
+ };
3832
5163
  /* isCollapsed and showShortcuts are used to control how the menu is displayed in a responsive environment and whether the shortcut keystrokes help should be displayed */
3833
5164
  $scope.isCollapsed = true;
3834
5165
  $scope.showShortcuts = false;
@@ -3856,8 +5187,8 @@ var fng;
3856
5187
  }
3857
5188
  }
3858
5189
  function filter(event) {
3859
- var tagName = (event.target || event.srcElement).tagName;
3860
- return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
5190
+ var tagName = (event.target).tagName;
5191
+ return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA' || tagName == "DIV" && event.target.classList.contains('ck-editor__editable'));
3861
5192
  }
3862
5193
  //console.log(event.keyCode, event.ctrlKey, event.shiftKey, event.altKey, event.metaKey);
3863
5194
  if (event.keyCode === 191 && (filter(event) || (event.ctrlKey && !event.altKey && !event.metaKey))) {
@@ -3886,7 +5217,7 @@ var fng;
3886
5217
  else if (event.keyCode === 45 && event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey) {
3887
5218
  deferredBtnClick('newButton'); // Ctrl+Shift+Ins creates New record
3888
5219
  }
3889
- else if (event.keyCode === 88 && event.ctrlKey && event.shiftKey && event.altKey && !event.metaKey) {
5220
+ else if (event.keyCode === 88 && event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey) {
3890
5221
  deferredBtnClick('deleteButton'); // Ctrl+Shift+X deletes record
3891
5222
  }
3892
5223
  };
@@ -3900,8 +5231,27 @@ var fng;
3900
5231
  }
3901
5232
  return result;
3902
5233
  };
5234
+ function initialiseContextMenu(menuCaption) {
5235
+ $scope.contextMenu = menuCaption;
5236
+ var menuId = "".concat(_.camelCase(menuCaption), "ContextMenu");
5237
+ // the context menu itself (see dropdown.ts) has an ng-if that checks for a value of
5238
+ // contextMenuId. let's delete this until we know we're ready to evaluate the security
5239
+ // of the menu items...
5240
+ $scope.contextMenuId = undefined;
5241
+ securityService.doSecurityWhenReady(function () {
5242
+ //... which we now are
5243
+ $scope.contextMenuId = menuId;
5244
+ $scope.contextMenuHidden = securityService.isSecurelyHidden($scope.contextMenuId);
5245
+ $scope.contextMenuDisabled = securityService.isSecurelyDisabled($scope.contextMenuId);
5246
+ });
5247
+ }
3903
5248
  $scope.$on('fngControllersLoaded', function (evt, sharedData, modelName) {
3904
- $scope.contextMenu = sharedData.dropDownDisplay || sharedData.modelNameDisplay || $filter('titleCase')(modelName, false);
5249
+ initialiseContextMenu(sharedData.dropDownDisplay || sharedData.modelNameDisplay || $filter('titleCase')(modelName, false));
5250
+ if (sharedData.dropDownDisplayPromise) {
5251
+ sharedData.dropDownDisplayPromise.then(function (value) {
5252
+ initialiseContextMenu(value);
5253
+ });
5254
+ }
3905
5255
  });
3906
5256
  $scope.$on('fngControllersUnloaded', function (evt) {
3907
5257
  clearContextMenu();
@@ -3917,28 +5267,60 @@ var fng;
3917
5267
  }
3918
5268
  else {
3919
5269
  // Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke
3920
- var args = item.args || [], fn = item.fn;
3921
- switch (args.length) {
3922
- case 0:
3923
- fn();
3924
- break;
3925
- case 1:
3926
- fn(args[0]);
3927
- break;
3928
- case 2:
3929
- fn(args[0], args[1]);
3930
- break;
3931
- case 3:
3932
- fn(args[0], args[1], args[2]);
3933
- break;
3934
- case 4:
3935
- fn(args[0], args[1], args[2], args[3]);
3936
- break;
5270
+ var args = item.args || [];
5271
+ var fn = item.fn;
5272
+ if (typeof fn === "function") {
5273
+ switch (args.length) {
5274
+ case 0:
5275
+ fn();
5276
+ break;
5277
+ case 1:
5278
+ fn(args[0]);
5279
+ break;
5280
+ case 2:
5281
+ fn(args[0], args[1]);
5282
+ break;
5283
+ case 3:
5284
+ fn(args[0], args[1], args[2]);
5285
+ break;
5286
+ case 4:
5287
+ fn(args[0], args[1], args[2], args[3]);
5288
+ break;
5289
+ }
5290
+ }
5291
+ else if (fn) {
5292
+ throw new Error("Incorrect menu setup");
3937
5293
  }
3938
5294
  }
3939
5295
  };
3940
5296
  $scope.isHidden = function (index) {
3941
- return $scope.items[index].isHidden ? $scope.items[index].isHidden() : false;
5297
+ function explicitlyHidden(item) {
5298
+ return item.isHidden ? item.isHidden() : false;
5299
+ }
5300
+ var dividerHide = false;
5301
+ var item = $scope.items[index];
5302
+ // Hide a divider if it appears under another
5303
+ if (item.divider) {
5304
+ if (index === 0) {
5305
+ dividerHide = true;
5306
+ }
5307
+ else {
5308
+ var foundVisible = false;
5309
+ var check = index - 1;
5310
+ while (check >= 0 && !dividerHide && !foundVisible) {
5311
+ if ($scope.items[check].divider) {
5312
+ dividerHide = true;
5313
+ }
5314
+ else if (!explicitlyHidden($scope.items[check])) {
5315
+ foundVisible = true;
5316
+ }
5317
+ else {
5318
+ --check;
5319
+ }
5320
+ }
5321
+ }
5322
+ }
5323
+ return dividerHide || explicitlyHidden(item);
3942
5324
  };
3943
5325
  $scope.isDisabled = function (index) {
3944
5326
  return $scope.items[index].isDisabled ? $scope.items[index].isDisabled() : false;
@@ -3957,6 +5339,10 @@ var fng;
3957
5339
  }
3958
5340
  return thisClass;
3959
5341
  };
5342
+ var originalTitle = $window.document.title;
5343
+ $scope.$on('$routeChangeSuccess', function () {
5344
+ $window.document.title = originalTitle;
5345
+ });
3960
5346
  }
3961
5347
  controllers.NavCtrl = NavCtrl;
3962
5348
  })(controllers = fng.controllers || (fng.controllers = {}));
@@ -3997,6 +5383,7 @@ var fng;
3997
5383
  ])
3998
5384
  .controller('BaseCtrl', fng.controllers.BaseCtrl)
3999
5385
  .controller('SaveChangesModalCtrl', fng.controllers.SaveChangesModalCtrl)
5386
+ .controller('LinkCtrl', fng.controllers.LinkCtrl)
4000
5387
  .controller('ModelCtrl', fng.controllers.ModelCtrl)
4001
5388
  .controller('NavCtrl', fng.controllers.NavCtrl)
4002
5389
  .directive('modelControllerDropdown', fng.directives.modelControllerDropdown)
@@ -4008,6 +5395,7 @@ var fng;
4008
5395
  .directive('fngNakedDate', fng.directives.fngNakedDate)
4009
5396
  .filter('camelCase', fng.filters.camelCase)
4010
5397
  .filter('titleCase', fng.filters.titleCase)
5398
+ .filter('extractTimestampFromMongoID', fng.filters.extractTimestampFromMongoID)
4011
5399
  .service('addAllService', fng.services.addAllService)
4012
5400
  .provider('cssFrameworkService', fng.services.cssFrameworkService)
4013
5401
  .provider('routingService', fng.services.routingService)
@@ -4018,16 +5406,21 @@ var fng;
4018
5406
  .factory('pluginHelper', fng.services.pluginHelper)
4019
5407
  .factory('recordHandler', fng.services.recordHandler)
4020
5408
  .factory('SchemasService', fng.services.SchemasService)
4021
- .factory('SubmissionsService', fng.services.SubmissionsService);
5409
+ .factory('SubmissionsService', fng.services.SubmissionsService)
5410
+ .factory('securityService', fng.services.securityService);
4022
5411
  })(fng || (fng = {}));
4023
5412
  // expose the library
4024
5413
  var formsAngular = fng.formsAngular;
4025
5414
 
4026
- angular.module('formsAngular').run(['$templateCache', function($templateCache) {$templateCache.put('base-analysis.html','<div ng-controller="AnalysisCtrl">\n <div class="container-fluid page-header report-header">\n <div ng-class="css(\'rowFluid\')">\n <div class="header-lhs col-xs-7 span7">\n <h1>{{ reportSchema.title }}</h1>\n </div>\n <div class="header-rhs col-xs-5 span5">\n <form-input schema="paramSchema" name="paramForm" ng-show="paramSchema" formstyle="horizontalCompact"></form-input>\n </div>\n </div>\n </div>\n <div class="container-fluid page-body report-body">\n <error-display></error-display>\n <div class="row-fluid">\n <div class="gridStyle" ui-grid="gridOptions" ui-grid-selection></div>\n </div>\n </div>\n</div>\n');
4027
- $templateCache.put('base-edit.html','<div ng-controller="BaseCtrl">\n <div ng-class="css(\'rowFluid\')" class="page-header edit-header">\n <div class="header-lhs col-sm-8 span8">\n <h4>{{modelNameDisplay}} :\n <span ng-repeat="field in listSchema">{{getListData(record, field.name)}} </span>\n </h4>\n </div>\n <div class="header-rhs col-sm-2 span2">\n <div form-buttons></div>\n </div>\n </div>\n <div class="container-fluid page-body edit-body">\n <error-display></error-display>\n <form-input name="baseForm" schema="baseSchema()" formstyle="compact"></form-input>\n </div>\n <!--{{ formSchema | json }}-->\n</div>\n');
4028
- $templateCache.put('base-list.html','<div ng-controller="BaseCtrl">\n <div ng-class="css(\'rowFluid\')" class="page-header list-header">\n <div class="header-lhs col-sm-8 span8">\n <h1>{{modelNameDisplay}}</h1>\n </div>\n <div class="header-rhs col-sm-2 span2">\n <a ng-href="{{generateNewUrl()}}"><button id="newBtn" class="btn btn-default"><i class="icon-plus"></i> New</button></a>\n </div>\n </div>\n <div class="page-body list-body">\n <error-display></error-display>\n <div ng-class="css(\'rowFluid\')" infinite-scroll="scrollTheList()">\n <a ng-repeat="record in recordList" ng-href="{{generateEditUrl(record)}}">\n <div class="list-item">\n <div ng-class="css(\'span\',12/listSchema.length)" ng-repeat="field in listSchema">{{getListData(record, field.name)}} </div>\n </div>\n </a>\n </div>\n </div>\n</div>\n');
5415
+ angular.module('formsAngular').run(['$templateCache', function($templateCache) {$templateCache.put('base-analysis.html','<div ng-controller="AnalysisCtrl">\n <error-display></error-display>\n <div ng-hide="!showLoading" class="loading">Loading&#8230;</div>\n <div class="container-fluid page-header report-header">\n <div ng-class="css(\'rowFluid\')">\n <div class="header-lhs col-xs-7 span7">\n <h1>{{ titleWithSubstitutions }}</h1>\n </div>\n <div class="header-rhs col-xs-5 span5">\n <form-input schema="paramSchema" name="paramForm" ng-show="paramSchema" formstyle="horizontalCompact"></form-input>\n </div>\n </div>\n </div>\n <div class="container-fluid page-body report-body">\n <div class="row-fluid report-grow">\n <div class="gridStyle" style="height:100%;" ui-grid="gridOptions" ui-grid-selection ui-grid-resize-columns></div>\n </div>\n </div>\n</div>\n');
5416
+ $templateCache.put('base-edit.html','<div ng-controller="BaseCtrl">\n <error-display></error-display>\n <div ng-hide="phase == \'ready\' && !showLoading" class="loading">Loading&#8230;</div>\n <div ng-class="css(\'rowFluid\')" class="page-header edit-header">\n <div class="header-lhs col-sm-8 span8">\n <h1 id="header-text">{{modelNameDisplay}} :\n <span id="header-data-desc">\n <span ng-show="!!editFormHeader" >{{ editFormHeader() }}</span>\n <span ng-hide="!!editFormHeader" ng-repeat="field in listSchema" ng-bind-html="getListData(record, field.name) + \' \'"></span>\n </span>\n </h1>\n </div>\n <div class="header-rhs col-sm-2 span2">\n <div form-buttons></div>\n </div>\n </div>\n <div class="container-fluid page-body edit-body">\n <form-input name="baseForm" schema="baseSchema()" formstyle="compact"></form-input>\n </div>\n<!-- <pre>-->\n <!--Record-->\n <!--{{ record | json }}-->\n <!--formSchema-->\n <!--{{ formSchema | json }}-->\n<!-- </pre>-->\n</div>\n');
5417
+ $templateCache.put('base-list-view.html','<div ng-controller="BaseCtrl">\n <error-display></error-display>\n <div ng-hide="!showLoading" class="loading">Loading&#8230;</div>\n <div ng-class="css(\'rowFluid\')" class="page-header list-header">\n <div class="header-lhs col-sm-8 span8">\n <h1>{{modelNameDisplay}}</h1>\n </div>\n </div>\n <div class="page-body list-body">\n <div ng-class="css(\'rowFluid\')" infinite-scroll="scrollTheList()">\n <a ng-repeat="record in recordList" ng-href="{{generateViewUrl(record)}}">\n <div class="list-item">\n <div ng-class="css(\'span\',12/listSchema.length)" ng-repeat="field in listSchema">{{getListData(record, field.name)}} </div>\n </div>\n </a>\n </div>\n </div>\n</div>\n');
5418
+ $templateCache.put('base-list.html','<div ng-controller="BaseCtrl">\n <error-display></error-display>\n <div ng-hide="!showLoading" class="loading">Loading&#8230;</div>\n <div ng-class="css(\'rowFluid\')" class="page-header list-header">\n <div class="header-lhs col-sm-8 span8">\n <h1>{{modelNameDisplay}}</h1>\n </div>\n <div class="header-rhs col-sm-2 span2">\n <a ng-href="{{generateNewUrl()}}"><button id="newBtn" class="btn btn-default"><i class="icon-plus"></i> New</button></a>\n </div>\n </div>\n <div class="page-body list-body">\n <div ng-class="css(\'rowFluid\')" infinite-scroll="scrollTheList()">\n <a ng-repeat="record in recordList" ng-href="{{generateEditUrl(record)}}">\n <div class="list-item">\n <div ng-class="css(\'span\',12/listSchema.length)" ng-repeat="field in listSchema">{{getListData(record, field.name)}} </div>\n </div>\n </a>\n </div>\n </div>\n</div>\n');
5419
+ $templateCache.put('base-view.html','<div ng-controller="BaseCtrl">\n <error-display></error-display>\n <div ng-hide="phase == \'ready\' && !spinning" class="loading">Loading&#8230;</div>\n <div ng-class="css(\'rowFluid\')" class="page-header edit-header">\n <div class="header-lhs col-sm-8 span8">\n <h1 id="header-text">{{modelNameDisplay}} :\n <span ng-repeat="field in listSchema" ng-bind-html="getListData(record, field.name) + \' \'"></span>\n </h1>\n </div>\n </div>\n <div class="container-fluid page-body edit-body">\n <form-input name="baseForm" schema="baseSchema()" formstyle="compact" viewform="true"></form-input>\n </div>\n</div>\n');
5420
+ $templateCache.put('error-display-bs2.html','<div id="display-error" ng-show="errorVisible" class="row-fluid ng-hide">\n <div class="alert alert-error offset1 span10">\n <button type="button" id="err-hide" class="close" ng-click="dismissError()"><i class="icon-remove"></i></button>\n <button type="button" id="err-pin" class="close" ng-click="stickError()"><i class="icon-eye-open"></i></button>\n <h4 id="err-title">{{alertTitle}}</h4>\n <div id="err-msg" ng-bind-html="errorMessage"></div>\n </div>\n</div>\n');
5421
+ $templateCache.put('error-display-bs3.html','<div id="display-error" ng-show="errorVisible" class="row ng-hide">\n <div class="alert alert-error col-md-offset-1 col-md-10 alert-danger">\n <button type="button" id="err-hide" class="close" ng-click="dismissError()"><i class="glyphicon glyphicon-remove"></i></button>\n <button type="button" id="err-pin" class="close" ng-click="stickError()"><i class="glyphicon glyphicon-pushpin"></i></button>\n <h4 id="err-title">{{alertTitle}}</h4>\n <div id="err-msg" ng-bind-html="errorMessage"></div>\n </div>\n</div>\n');
4029
5422
  $templateCache.put('error-messages.html','<div ng-message="required">A value is required for this field</div>\n<div ng-message="minlength">Too few characters entered</div>\n<div ng-message="maxlength">Too many characters entered</div>\n<div ng-message="min">That value is too small</div>\n<div ng-message="max">That value is too large</div>\n<div ng-message="email">You need to enter a valid email address</div>\n<div ng-message="pattern">This field does not match the expected pattern</div>\n');
4030
- $templateCache.put('form-button-bs2.html','<div class="form-btn-grp">\n <div class="btn-group pull-right">\n <button id="saveButton" class="btn btn-mini btn-primary form-btn" ng-click="save()" ng-disabled="isSaveDisabled()"><i class="icon-ok"></i> Save</button>\n <button id="cancelButton" class="btn btn-mini btn-warning form-btn" ng-click="cancel()" ng-disabled="isCancelDisabled()"><i class="icon-remove"></i> Cancel</button>\n </div>\n <div class="btn-group pull-right">\n <button id="newButton" class="btn btn-mini btn-success form-btn" ng-click="newClick()" ng-disabled="isNewDisabled()"><i class="icon-plus"></i> New</button>\n <button id="deleteButton" class="btn btn-mini btn-danger form-btn" ng-click="deleteClick()" ng-disabled="isDeleteDisabled()"><i class="icon-minus"></i> Delete</button>\n </div>\n</div>\n');
4031
- $templateCache.put('form-button-bs3.html','<div class="form-btn-grp">\n <div class="btn-group pull-right">\n <button id="saveButton" class="btn btn-primary form-btn btn-xs" ng-click="save()" ng-disabled="isSaveDisabled()"><i class="glyphicon glyphicon-ok"></i> Save</button>\n <button id="cancelButton" class="btn btn-warning form-btn btn-xs" ng-click="cancel()" ng-disabled="isCancelDisabled()"><i class="glyphicon glyphicon-remove"></i> Cancel</button>\n </div>\n <div class="btn-group pull-right">\n <button id="newButton" class="btn btn-success form-btn btn-xs" ng-click="newClick()" ng-disabled="isNewDisabled()"><i class="glyphicon glyphicon-plus"></i> New</button>\n <button id="deleteButton" class="btn btn-danger form-btn btn-xs" ng-click="deleteClick()" ng-disabled="isDeleteDisabled()"><i class="glyphicon glyphicon-minus"></i> Delete</button>\n </div>\n</div>\n');
4032
- $templateCache.put('search-bs2.html','<form class="navbar-search pull-right">\n <div id="search-cg" class="control-group" ng-class="errorClass">\n <input type="text" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:250}" class="search-query" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <span ng-class="resultClass($index)" ng-click="selectResult($index)">{{result.resourceText}} {{result.text}}</span>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');
4033
- $templateCache.put('search-bs3.html','<form class="pull-right navbar-form">\n <div id="search-cg" class="form-group" ng-class="errorClass">\n <input type="text" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:250}" class="search-query form-control" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <span ng-class="resultClass($index)" ng-click="selectResult($index)" title={{result.additional}}>{{result.resourceText}} {{result.text}}</span>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');}]);
5423
+ $templateCache.put('form-button-bs2.html','<div class="form-btn-grp">\n <div class="btn-group pull-right">\n <button id="saveButton" class="btn btn-mini btn-primary form-btn" ng-click="save()" ng-disabled="isSaveDisabled()"><i class="icon-ok"></i> Save</button>\n <div id="why-disabled" ng-class="{showwhy:!!whyDisabled}" ng-bind-html="whyDisabled"></div>\n <button id="cancelButton" class="btn btn-mini btn-warning form-btn" ng-click="cancel()" ng-disabled="isCancelDisabled()"><i class="icon-remove"></i> Cancel</button>\n </div>\n <div class="btn-group pull-right">\n <button id="newButton" class="btn btn-mini btn-success form-btn" ng-click="newClick()" ng-disabled="isNewDisabled()"><i class="icon-plus"></i> New</button>\n <button id="deleteButton" class="btn btn-mini btn-danger form-btn" ng-click="deleteClick()" ng-disabled="isDeleteDisabled()"><i class="icon-minus"></i> Delete</button>\n </div>\n</div>\n');
5424
+ $templateCache.put('form-button-bs3.html','<div class="form-btn-grp">\n <div class="btn-group pull-right">\n <button id="saveButton" class="btn btn-primary form-btn btn-xs" ng-click="save()" ng-disabled="isSaveDisabled()"><i class="glyphicon glyphicon-ok"></i> Save</button>\n <div id="why-disabled" ng-class="{showwhy:!!whyDisabled}" ng-bind-html="whyDisabled"></div>\n <button id="cancelButton" class="btn btn-warning form-btn btn-xs" ng-click="cancel()" ng-disabled="isCancelDisabled()"><i class="glyphicon glyphicon-remove"></i> Cancel</button>\n </div>\n <div class="btn-group pull-right">\n <button id="newButton" class="btn btn-success form-btn btn-xs" ng-click="newClick()" ng-disabled="isNewDisabled()"><i class="glyphicon glyphicon-plus"></i> New</button>\n <button id="deleteButton" class="btn btn-danger form-btn btn-xs" ng-click="deleteClick()" ng-disabled="isDeleteDisabled()"><i class="glyphicon glyphicon-minus"></i> Delete</button>\n </div>\n</div>\n');
5425
+ $templateCache.put('search-bs2.html','<form class="navbar-search pull-right">\n <div id="search-cg" class="control-group" ng-class="errorClass">\n <input type="text" spellcheck="false" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:250}" class="search-query" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <a href="{{result.href}}" ng-class="resultClass($index)" title="{{result.additional}}">{{result.resourceText}} {{result.text}}</a>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');
5426
+ $templateCache.put('search-bs3.html','<form class="pull-right navbar-form">\n <div id="search-cg" class="form-group" ng-class="errorClass">\n <input type="text" spellcheck="false" autocomplete="off" id="searchinput" ng-model="searchTarget" ng-model-options="{debounce:250}" class="search-query form-control" placeholder="{{searchPlaceholder}}" ng-keyup="handleKey($event)">\n </div>\n</form>\n<div class="results-container" ng-show="results.length >= 1">\n <div class="search-results">\n <div ng-repeat="result in results">\n <a href="{{result.href}}" ng-class="resultClass($index)" title="{{result.additional}}">{{result.resourceText}} {{result.text}}</a>\n </div>\n <div ng-show="moreCount > 0">(plus more - continue typing to narrow down search...)\n </div>\n </div>\n</div>\n');}]);