forms-angular 0.12.0-beta.192 → 0.12.0-beta.194

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>' +
48
+ template: "<li id=\"{{ contextMenuId }}\" ng-show=\"items.length > 0\" class=\"mcdd\" uib-dropdown ".concat(menuVisibilityStr, " ").concat(menuDisabledStr, ">") +
12
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)">' +
53
+ " <li ng-repeat=\"choice in items\" ng-attr-id=\"{{choice.id}}\" ".concat(itemVisibilityStr, " ").concat(itemDisabledStr, ">") +
17
54
  ' <a ng-show="choice.text || choice.textFunc" class="dropdown-option" ng-href="{{choice.url || choice.urlFunc()}}" ng-click="doClick($index, $event)">' +
18
- ' {{ choice.text || choice.textFunc() }}' +
19
- ' </a>' +
20
- ' </li>' +
21
- ' </ul>' +
22
- '</li>'
55
+ " {{ choice.text || choice.textFunc() }}" +
56
+ " </a>" +
57
+ " </li>" +
58
+ " </ul>" +
59
+ "</li>",
23
60
  };
24
61
  }
25
62
  directives.modelControllerDropdown = modelControllerDropdown;
@@ -236,7 +273,7 @@ var fng;
236
273
  (function (fng) {
237
274
  var directives;
238
275
  (function (directives) {
239
- formInput.$inject = ["$compile", "$rootScope", "$filter", "$timeout", "cssFrameworkService", "formGenerator", "formMarkupHelper"];
276
+ formInput.$inject = ["$compile", "$rootScope", "$filter", "$timeout", "cssFrameworkService", "formGenerator", "formMarkupHelper", "securityService"];
240
277
  var tabsSetupState;
241
278
  (function (tabsSetupState) {
242
279
  tabsSetupState[tabsSetupState["Y"] = 0] = "Y";
@@ -244,7 +281,7 @@ var fng;
244
281
  tabsSetupState[tabsSetupState["Forced"] = 2] = "Forced";
245
282
  })(tabsSetupState || (tabsSetupState = {}));
246
283
  /*@ngInject*/
247
- function formInput($compile, $rootScope, $filter, $timeout, cssFrameworkService, formGenerator, formMarkupHelper) {
284
+ function formInput($compile, $rootScope, $filter, $timeout, cssFrameworkService, formGenerator, formMarkupHelper, securityService) {
248
285
  return {
249
286
  restrict: 'EA',
250
287
  link: function (scope, element, attrs) {
@@ -288,7 +325,7 @@ var fng;
288
325
  // <input type="text" class="input-small" placeholder="Email">
289
326
  var subkeys = [];
290
327
  var tabsSetup = tabsSetupState.N;
291
- var generateInput = function (fieldInfo, modelString, isRequired, idString, options) {
328
+ var generateInput = function (fieldInfo, modelString, isRequired, options) {
292
329
  function generateEnumInstructions() {
293
330
  var enumInstruction;
294
331
  if (angular.isArray(scope[fieldInfo.options])) {
@@ -314,6 +351,10 @@ var fng;
314
351
  }
315
352
  return enumInstruction;
316
353
  }
354
+ var idString = fieldInfo.id;
355
+ if (fieldInfo.array || options.subschema) {
356
+ idString = formMarkupHelper.generateArrayElementIdString(idString, fieldInfo, options);
357
+ }
317
358
  var nameString;
318
359
  if (!modelString) {
319
360
  var modelBase = (options.model || 'record') + '.';
@@ -335,7 +376,6 @@ var fng;
335
376
  }
336
377
  else {
337
378
  modelString += '[$index].' + lastPart;
338
- idString = null;
339
379
  nameString = compoundName.replace(/\./g, '-');
340
380
  }
341
381
  }
@@ -350,28 +390,13 @@ var fng;
350
390
  isRequired = isRequired || fieldInfo.required;
351
391
  var requiredStr = isRequired ? ' required ' : '';
352
392
  var enumInstruction;
353
- function handleReadOnlyDisabled(readonly) {
354
- var retVal = '';
355
- if (readonly) {
356
- // despite the option being "readonly", we should use disabled and ng-disabled rather than their readonly
357
- // equivalents (which give controls the appearance of being read-only, but don't actually prevent user
358
- // interaction)
359
- if (typeof readonly === "boolean") {
360
- retVal = " disabled ";
361
- }
362
- else {
363
- retVal = " ng-disabled=\"".concat(readonly, "\" ");
364
- }
365
- }
366
- return retVal;
367
- }
368
393
  switch (fieldInfo.type) {
369
394
  case 'select':
370
395
  if (fieldInfo.select2) {
371
396
  value = '<input placeholder="fng-select2 has been removed" readonly>';
372
397
  }
373
398
  else {
374
- common += handleReadOnlyDisabled(fieldInfo.readonly);
399
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
375
400
  common += fieldInfo.add ? (' ' + fieldInfo.add + ' ') : '';
376
401
  common += " aria-label=\"".concat(fieldInfo.label && fieldInfo.label !== "" ? fieldInfo.label : fieldInfo.name, "\" ");
377
402
  value = '<select ' + common + 'class="' + allInputsVars.formControl.trim() + allInputsVars.compactClass + allInputsVars.sizeClassBS2 + '" ' + requiredStr + '>';
@@ -425,7 +450,8 @@ var fng;
425
450
  case 'radio':
426
451
  value = '';
427
452
  common += requiredStr;
428
- common += handleReadOnlyDisabled(fieldInfo.readonly);
453
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
454
+ ;
429
455
  common += fieldInfo.add ? (' ' + fieldInfo.add + ' ') : '';
430
456
  var separateLines = options.formstyle === 'vertical' || (options.formstyle !== 'inline' && !fieldInfo.inlineRadio);
431
457
  if (angular.isArray(fieldInfo.options)) {
@@ -456,7 +482,8 @@ var fng;
456
482
  break;
457
483
  case 'checkbox':
458
484
  common += requiredStr;
459
- common += handleReadOnlyDisabled(fieldInfo.readonly);
485
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
486
+ ;
460
487
  common += fieldInfo.add ? (' ' + fieldInfo.add + ' ') : '';
461
488
  value = formMarkupHelper.generateSimpleInput(common, fieldInfo, options);
462
489
  if (cssFrameworkService.framework() === 'bs3') {
@@ -465,6 +492,8 @@ var fng;
465
492
  break;
466
493
  default:
467
494
  common += formMarkupHelper.addTextInputMarkup(allInputsVars, fieldInfo, requiredStr);
495
+ common += formMarkupHelper.handleReadOnlyDisabled(fieldInfo, scope).join(" ");
496
+ ;
468
497
  if (fieldInfo.type === 'textarea') {
469
498
  if (fieldInfo.rows) {
470
499
  if (fieldInfo.rows === 'auto') {
@@ -514,11 +543,12 @@ var fng;
514
543
  return result;
515
544
  };
516
545
  var containerInstructions = function (info) {
517
- var result = { before: '', after: '' };
546
+ var result;
518
547
  if (typeof info.containerType === 'function') {
519
548
  result = info.containerType(info);
520
549
  }
521
550
  else {
551
+ result = {};
522
552
  switch (info.containerType) {
523
553
  case 'tab':
524
554
  var tabNo = -1;
@@ -531,12 +561,26 @@ var fng;
531
561
  if (tabNo >= 0) {
532
562
  // TODO Figure out tab history updates (check for other tab-history-todos)
533
563
  // result.before = '<uib-tab deselect="tabDeselect($event, $selectedIndex)" select="updateQueryForTab(\'' + info.title + '\')" heading="' + info.title + '"'
534
- result.before = '<uib-tab deselect="tabDeselect($event, $selectedIndex)" select="updateQueryForTab(\'' + info.title + '\')" heading="' + info.title + '"';
535
- if (tabNo > 0) {
536
- 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>';
537
583
  }
538
- result.before += '>';
539
- result.after = '</uib-tab>';
540
584
  }
541
585
  else {
542
586
  result.before = '<p>Error! Tab ' + info.title + ' not found in tab list</p>';
@@ -629,6 +673,9 @@ var fng;
629
673
  };
630
674
  var handleField = function (info, options) {
631
675
  var fieldChrome = formMarkupHelper.fieldChrome(scope, info, options);
676
+ if (fieldChrome.omit) {
677
+ return "";
678
+ }
632
679
  var template = fieldChrome.template;
633
680
  if (info.schema) {
634
681
  var niceName = info.name.replace(/\./g, '_');
@@ -685,22 +732,44 @@ var fng;
685
732
  if (info.formStyle === "inline" && info.inlineHeaders) {
686
733
  template += generateInlineHeaders(info.schema, options, model, info.inlineHeaders === "always");
687
734
  }
688
- template += '<ol class="sub-doc"' + (info.sortable ? " ui-sortable=\"sortableOptions\" ng-model=\"".concat(model, "\"") : '') + '>';
689
- template += '<li ng-form class="' + (cssFrameworkService.framework() === 'bs2' ? 'row-fluid ' : '') +
690
- (info.inlineHeaders ? 'width-controlled ' : '') +
691
- convertFormStyleToClass(info.formStyle) + ' ' + (info.ngClass ? "ng-class:" + info.ngClass : "") + '" name="form_' + niceName + '{{$index}}" class="sub-doc well" id="' + info.id + 'List_{{$index}}" ' +
692
- ' ng-repeat="subDoc in ' + model + ' track by $index"' +
693
- (info.filterable ? ' data-ng-hide="subDoc._hidden"' : "") + '>';
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
+ ">";
694
760
  if (cssFrameworkService.framework() === 'bs2') {
695
761
  template += '<div class="row-fluid sub-doc">';
696
762
  }
697
763
  if (info.noRemove !== true || info.customSubDoc) {
698
- template += ' <div class="sub-doc-btns">';
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 : "", ">");
699
768
  if (typeof info.customSubDoc == 'string') {
700
769
  template += info.customSubDoc;
701
770
  }
702
771
  if (info.noRemove !== true) {
703
- template += "<button ".concat(info.noRemove ? 'ng-hide="' + info.noRemove + '"' : '', " name=\"remove_").concat(info.id, "_btn\" ng-click=\"remove('").concat(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)\"");
704
773
  if (info.remove) {
705
774
  template += ' class="remove-btn btn btn-mini btn-default btn-xs form-btn"><i class="' + formMarkupHelper.glyphClass() + '-minus"></i> Remove';
706
775
  }
@@ -755,7 +824,12 @@ var fng;
755
824
  else {
756
825
  hideCond = info.noAdd ? "ng-hide=\"".concat(info.noAdd, "\"") : '';
757
826
  indicatorShowCond = info.noAdd ? "ng-show=\"".concat(info.noAdd, " && ").concat(indicatorShowCond, "\"") : '';
758
- footer += "<button ".concat(hideCond, " id=\"add_").concat(info.id, "_btn\" class=\"add-btn btn btn-default btn-xs btn-mini\" ng-click=\"add('").concat(info.name, "',$event)\">\n <i class=\"' + formMarkupHelper.glyphClass() + '-plus\"></i> \n Add\n </button>");
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>";
759
833
  }
760
834
  if (info.noneIndicator) {
761
835
  footer += "<span ".concat(indicatorShowCond, " class=\"none_indicator\" id=\"no_").concat(info.id, "_indicator\">None</span>");
@@ -790,12 +864,16 @@ var fng;
790
864
  throw new Error('Cannot use arrays in an inline or stacked form');
791
865
  }
792
866
  template += formMarkupHelper.label(scope, info, info.type !== 'link', options);
793
- 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);
794
872
  }
795
873
  else {
796
874
  // Single fields here
797
875
  template += formMarkupHelper.label(scope, info, null, options);
798
- template += formMarkupHelper.handleInputAndControlDiv(generateInput(info, null, options.required, info.id, options), controlDivClasses);
876
+ template += formMarkupHelper.handleInputAndControlDiv(generateInput(info, null, options.required, options), controlDivClasses);
799
877
  }
800
878
  }
801
879
  template += fieldChrome.closeTag;
@@ -924,6 +1002,8 @@ var fng;
924
1002
  callHandleField = false;
925
1003
  }
926
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...
927
1007
  var parts = containerInstructions(info);
928
1008
  switch (info.containerType) {
929
1009
  case 'tab':
@@ -934,9 +1014,12 @@ var fng;
934
1014
  var activeTabNo = _.findIndex(scope.tabs, function (tab) { return (tab.active); });
935
1015
  scope.activeTabNo = activeTabNo >= 0 ? activeTabNo : 0;
936
1016
  }
937
- result += parts.before;
938
- result += processInstructions(info.content, null, options);
939
- 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
+ }
940
1023
  break;
941
1024
  case 'tabset':
942
1025
  tabsSetup = tabsSetupState.Y;
@@ -1506,6 +1589,10 @@ var fng;
1506
1589
  needDivider = false;
1507
1590
  parentScope.items.push({ divider: true });
1508
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
+ }
1509
1596
  parentScope.items.push(value);
1510
1597
  }
1511
1598
  });
@@ -1813,7 +1900,7 @@ var fng;
1813
1900
  * All methods should be state-less
1814
1901
  *
1815
1902
  */
1816
- function formGenerator($location, $timeout, $filter, routingService, recordHandler) {
1903
+ function formGenerator($filter, routingService, recordHandler, securityService) {
1817
1904
  function handleSchema(description, source, destForm, destList, prefix, doRecursion, $scope, ctrlState) {
1818
1905
  function handletabInfo(tabName, thisInst) {
1819
1906
  var tabTitle = angular.copy(tabName);
@@ -1950,7 +2037,7 @@ var fng;
1950
2037
  angular.extend(formInstructions, mongooseType.options.form);
1951
2038
  }
1952
2039
  }
1953
- if (mongooseType.instance === 'String') {
2040
+ if (mongooseType.instance === 'String' || (mongooseType.instance === 'ObjectID' && formInstructions.asText)) {
1954
2041
  if (mongooseOptions.enum) {
1955
2042
  formInstructions.type = formInstructions.type || 'select';
1956
2043
  if (formInstructions.select2) {
@@ -2261,14 +2348,19 @@ var fng;
2261
2348
  return forceNextTime;
2262
2349
  },
2263
2350
  add: function add(fieldName, $event, $scope, modelOverride) {
2264
- var _a;
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
+ }
2265
2357
  // check that target element is visible. May not be reliable - see https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
2266
2358
  if ($event.target.offsetParent) {
2267
2359
  var arrayField = getArrayFieldToExtend(fieldName, $scope, modelOverride);
2268
2360
  var schemaElement = $scope.formSchema.find(function (f) { return f.name === fieldName; }); // In case someone is using the formSchema directly
2269
2361
  var subSchema = schemaElement ? schemaElement.schema : null;
2270
2362
  var obj = subSchema ? $scope.setDefaults(subSchema, fieldName + '.') : {};
2271
- if (typeof ((_a = $scope.dataEventFunctions) === null || _a === void 0 ? void 0 : _a.onInitialiseNewSubDoc) === "function") {
2363
+ if (typeof ((_b = $scope.dataEventFunctions) === null || _b === void 0 ? void 0 : _b.onInitialiseNewSubDoc) === "function") {
2272
2364
  $scope.dataEventFunctions.onInitialiseNewSubDoc(fieldName, subSchema, obj);
2273
2365
  }
2274
2366
  arrayField.push(obj);
@@ -2281,6 +2373,12 @@ var fng;
2281
2373
  $scope.setFormDirty($event);
2282
2374
  },
2283
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
+ }
2284
2382
  // Remove an element from an array
2285
2383
  var arrayField = getArrayFieldToExtend(fieldName, $scope, modelOverride);
2286
2384
  var err;
@@ -2329,7 +2427,7 @@ var fng;
2329
2427
  }
2330
2428
  return result;
2331
2429
  },
2332
- decorateScope: function decorateScope($scope, formGeneratorInstance, recordHandlerInstance, sharedData) {
2430
+ decorateScope: function decorateScope($scope, formGeneratorInstance, recordHandlerInstance, sharedData, pseudoUrl) {
2333
2431
  $scope.record = sharedData.record;
2334
2432
  $scope.phase = 'init';
2335
2433
  $scope.disableFunctions = sharedData.disableFunctions;
@@ -2346,6 +2444,7 @@ var fng;
2346
2444
  $scope.pageSize = 60;
2347
2445
  $scope.pagesLoaded = 0;
2348
2446
  sharedData.baseScope = $scope;
2447
+ securityService.decorateSecurableScope($scope, { pseudoUrl: pseudoUrl });
2349
2448
  $scope.generateEditUrl = function (obj) {
2350
2449
  return formGeneratorInstance.generateEditUrl(obj, $scope);
2351
2450
  };
@@ -2356,7 +2455,20 @@ var fng;
2356
2455
  return formGeneratorInstance.generateNewUrl($scope);
2357
2456
  };
2358
2457
  $scope.scrollTheList = function () {
2359
- 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
+ }
2360
2472
  };
2361
2473
  $scope.getListData = function (record, fieldName) {
2362
2474
  return recordHandlerInstance.getListData(record, fieldName, $scope.listSchema, $scope);
@@ -2406,7 +2518,7 @@ var fng;
2406
2518
  };
2407
2519
  }
2408
2520
  services.formGenerator = formGenerator;
2409
- formGenerator.$inject = ["$location", "$timeout", "$filter", "routingService", "recordHandler"];
2521
+ formGenerator.$inject = ["$filter", "routingService", "recordHandler", "securityService"];
2410
2522
  })(services = fng.services || (fng.services = {}));
2411
2523
  })(fng || (fng = {}));
2412
2524
  /// <reference path="../../index.d.ts" />
@@ -2415,8 +2527,8 @@ var fng;
2415
2527
  var services;
2416
2528
  (function (services) {
2417
2529
  /*@ngInject*/
2418
- formMarkupHelper.$inject = ["cssFrameworkService", "inputSizeHelper", "addAllService", "$filter"];
2419
- function formMarkupHelper(cssFrameworkService, inputSizeHelper, addAllService, $filter) {
2530
+ formMarkupHelper.$inject = ["cssFrameworkService", "inputSizeHelper", "addAllService", "securityService", "$filter"];
2531
+ function formMarkupHelper(cssFrameworkService, inputSizeHelper, addAllService, securityService, $filter) {
2420
2532
  function generateNgShow(showWhen, model) {
2421
2533
  function evaluateSide(side) {
2422
2534
  var result = side;
@@ -2454,25 +2566,180 @@ var fng;
2454
2566
  function glyphClass() {
2455
2567
  return (cssFrameworkService.framework() === 'bs2' ? 'icon' : 'glyphicon glyphicon');
2456
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;
2688
+ }
2457
2689
  return {
2458
2690
  isHorizontalStyle: isHorizontalStyle,
2691
+ isArrayElement: isArrayElement,
2459
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
+ }
2460
2731
  var classes = info.classes || '';
2461
2732
  var template = '';
2462
2733
  var closeTag = '';
2463
- var insert = '';
2464
2734
  info.showWhen = info.showWhen || info.showwhen; // deal with use within a directive
2465
2735
  if (info.showWhen) {
2466
2736
  if (typeof info.showWhen === 'string') {
2467
- insert += 'ng-show="' + info.showWhen + '"';
2737
+ insert += ' ng-show="' + info.showWhen + '"';
2468
2738
  }
2469
2739
  else {
2470
- insert += 'ng-show="' + generateNgShow(info.showWhen, options.model) + '"';
2740
+ insert += ' ng-show="' + generateNgShow(info.showWhen, options.model) + '"';
2471
2741
  }
2472
2742
  }
2473
- if (info.id && typeof info.id.replace === "function") {
2474
- insert += ' id="cg_' + info.id.replace(/\./g, '-') + '"';
2475
- }
2476
2743
  if (cssFrameworkService.framework() === 'bs3') {
2477
2744
  classes += ' form-group';
2478
2745
  if (options.formstyle === 'vertical' && info.size !== 'block-level') {
@@ -2532,7 +2799,8 @@ var fng;
2532
2799
  }
2533
2800
  labelHTML += addAllService.addAll(scope, 'Label', null, options) + ' class="' + classes + '">' + fieldInfo.label;
2534
2801
  if (addButtonMarkup) {
2535
- 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>");
2536
2804
  }
2537
2805
  labelHTML += '</label>';
2538
2806
  if (fieldInfo.linklabel) {
@@ -2568,13 +2836,24 @@ var fng;
2568
2836
  if (['inline', 'stacked'].includes(options.formstyle)) {
2569
2837
  placeHolder = placeHolder || fieldInfo.label;
2570
2838
  }
2571
- common = 'data-ng-model="' + modelString + '"' + (idString ? ' id="' + idString + '" name="' + idString + '" ' : ' name="' + nameString + '" ');
2572
- 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
+ }
2573
2852
  if (fieldInfo.popup) {
2574
- common += 'title="' + fieldInfo.popup + '" ';
2853
+ common += " title=\"".concat(fieldInfo.popup, "\"");
2575
2854
  }
2576
2855
  if (fieldInfo.ariaLabel) {
2577
- common += 'aria-label="' + fieldInfo.ariaLabel + '" ';
2856
+ common += " aria-label=\"".concat(fieldInfo.ariaLabel, "\"");
2578
2857
  }
2579
2858
  common += addAllService.addAll(scope, 'Field', null, options);
2580
2859
  return {
@@ -2595,6 +2874,9 @@ var fng;
2595
2874
  var helpMarkup = cssFrameworkService.framework() === 'bs2' ? { el: 'span', cl: 'help-inline' } : { el: 'div', cl: 'help-block' };
2596
2875
  value += "<".concat(helpMarkup.el, " class=\"").concat(helpMarkup.cl, "\">").concat(inlineHelp, "</").concat(helpMarkup.el, ">");
2597
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/>";
2598
2880
  if (!options.noid) {
2599
2881
  value += "<div ng-if=\"".concat((options.name || 'myForm'), "['").concat(fieldInfo.id, "'].$dirty\" class=\"help-block\">") +
2600
2882
  " <div ng-messages=\"".concat((options.name || 'myForm'), "['").concat(fieldInfo.id, "'].$error\">") +
@@ -2635,18 +2917,21 @@ var fng;
2635
2917
  }
2636
2918
  return inputMarkup;
2637
2919
  },
2638
- handleArrayInputAndControlDiv: function handleArrayInputAndControlDiv(inputMarkup, controlDivClasses, info, options) {
2639
- var result = '<div ';
2640
- if (cssFrameworkService.framework() === 'bs3') {
2641
- result += 'ng-class="skipCols($index)" ';
2642
- }
2643
- result += 'class="' + controlDivClasses.join(' ') + '" id="' + info.id + 'List" ';
2644
- result += 'ng-repeat="arrayItem in ' + (options.model || 'record') + '.' + info.name + ' track by $index">';
2645
- result += inputMarkup;
2646
- if (info.type !== 'link') {
2647
- result += '<i ng-click="remove(\'' + info.name + '\',$index,$event)" id="remove_' + info.id + '_{{$index}}" class="' + glyphClass() + '-minus-sign"></i>';
2648
- }
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);
2649
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
+ }
2650
2935
  return result;
2651
2936
  },
2652
2937
  addTextInputMarkup: function addTextInputMarkup(allInputsVars, fieldInfo, requiredStr) {
@@ -2659,14 +2944,11 @@ var fng;
2659
2944
  result += ' ' + fieldInfo.add + ' ';
2660
2945
  }
2661
2946
  result += requiredStr;
2662
- if (fieldInfo.readonly) {
2663
- result += " ".concat(typeof fieldInfo.readOnly === 'boolean' ? 'readonly' : 'ng-readonly="' + fieldInfo.readonly + '"', " ");
2664
- }
2665
- else {
2666
- result += ' ';
2667
- }
2668
2947
  return result;
2669
- }
2948
+ },
2949
+ handleReadOnlyDisabled: handleReadOnlyDisabled,
2950
+ generateArrayElementIdString: generateArrayElementIdString,
2951
+ genDisableableAncestorStr: genDisableableAncestorStr
2670
2952
  };
2671
2953
  }
2672
2954
  services.formMarkupHelper = formMarkupHelper;
@@ -2705,105 +2987,260 @@ var fng;
2705
2987
  /*@ngInject*/
2706
2988
  pluginHelper.$inject = ["formMarkupHelper"];
2707
2989
  function pluginHelper(formMarkupHelper) {
2708
- return {
2709
- extractFromAttr: function extractFromAttr(attr, directiveName) {
2710
- function deserialize(str) {
2711
- var retVal = str.replace(/&quot;/g, '"');
2712
- if (retVal === 'true') {
2713
- 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"';
2714
3027
  }
2715
- else if (retVal === 'false') {
2716
- 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, "\"");
2717
3099
  }
2718
- else if (!isNaN(parseFloat(retVal)) && isFinite(retVal)) {
2719
- 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;
2720
3125
  }
2721
- return retVal;
2722
3126
  }
2723
- var info = {};
2724
- var options = { formStyle: attr.formstyle };
2725
- var directiveOptions = {};
2726
- var directiveNameLength = directiveName ? directiveName.length : 0;
2727
- var lcDirectiveName = directiveName === null || directiveName === void 0 ? void 0 : directiveName.toLowerCase();
2728
- for (var prop in attr) {
2729
- if (attr.hasOwnProperty(prop)) {
2730
- var lcProp = prop.toLowerCase();
2731
- if (lcProp.slice(0, 6) === 'fngfld') {
2732
- info[lcProp.slice(6)] = deserialize(attr[prop]);
2733
- }
2734
- else if (lcProp.slice(0, 6) === 'fngopt') {
2735
- options[lcProp.slice(6)] = deserialize(attr[prop]);
2736
- }
2737
- else if (directiveName && lcProp.slice(0, directiveNameLength) === lcDirectiveName) {
2738
- directiveOptions[_.kebabCase(prop.slice(directiveNameLength))] = deserialize(attr[prop]);
2739
- }
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]);
2740
3144
  }
2741
3145
  }
2742
- return { info: info, options: options, directiveOptions: directiveOptions };
2743
- },
2744
- buildInputMarkup: function buildInputMarkup(scope, model, info, options, addButtons, needsX, generateInputControl) {
2745
- var fieldChrome = formMarkupHelper.fieldChrome(scope, info, options, ' id="cg_' + info.id + '"');
2746
- var controlDivClasses = formMarkupHelper.controlDivClasses(options);
2747
- var elementHtml = fieldChrome.template + formMarkupHelper.label(scope, info, addButtons, options);
2748
- var modelString, idString, nameString;
2749
- if (addButtons) {
2750
- modelString = 'arrayItem' + (needsX ? '.x' : '');
2751
- idString = info.id + '_{{$index}}';
2752
- 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);
2753
3168
  }
2754
- else {
2755
- modelString = model + '.' + info.name;
2756
- idString = info.id;
2757
- 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 "";
2758
3176
  }
2759
- 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) {
2760
3188
  // Schema handling - need to massage the ngModel and the id
2761
- var modelBase = model + '.';
2762
- var compoundName = info.name;
3189
+ var modelBase = attrs.model + ".";
2763
3190
  var root = options.subschemaroot;
2764
- var lastPart = compoundName.slice(root.length + 1);
2765
- modelString = modelBase;
2766
- if (options.index) {
2767
- modelString += root + '[' + options.index + '].' + lastPart;
2768
- 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;
2769
3196
  }
2770
3197
  else {
2771
- modelString += root;
2772
- if (options.subkey) {
2773
- idString = modelString.slice(modelBase.length).replace(/\./g, '-') + '-subkey' + options.subkeyno + '-' + lastPart;
2774
- modelString += '[' + '$_arrayOffset_' + root.replace(/\./g, '_') + '_' + options.subkeyno + '].' + lastPart;
2775
- }
2776
- else {
2777
- modelString += '[$index].' + lastPart;
2778
- idString = null;
2779
- nameString = compoundName.replace(/\./g, '-');
2780
- }
3198
+ modelString += "[$index]." + lastPart;
3199
+ nameString = info.name.replace(/\./g, "-");
2781
3200
  }
2782
3201
  }
2783
3202
  var buildingBlocks = formMarkupHelper.allInputsVars(scope, info, options, modelString, idString, nameString);
2784
3203
  buildingBlocks.modelString = modelString;
2785
- 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
+ }
2786
3216
  elementHtml += fieldChrome.closeTag;
2787
3217
  return elementHtml;
2788
3218
  },
2789
- findIdInSchemaAndFlagNeedX: function findIdInSchemaAndFlagNeedX(scope, id) {
2790
- // Find the entry in the schema of scope for id and add a needsX property so string arrays are properly handled
2791
- var foundIt = false;
2792
- for (var i = 0; i < scope.length; i++) {
2793
- var element = scope[i];
2794
- if (element.id === id) {
2795
- element.needsX = true;
2796
- foundIt = true;
2797
- break;
2798
- }
2799
- else if (element.schema) {
2800
- if (findIdInSchemaAndFlagNeedX(element.schema, id)) {
2801
- foundIt = true;
2802
- break;
2803
- }
2804
- }
2805
- }
2806
- 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);
2807
3244
  }
2808
3245
  };
2809
3246
  }
@@ -2822,8 +3259,8 @@ var fng;
2822
3259
  *
2823
3260
  */
2824
3261
  /*@ngInject*/
2825
- recordHandler.$inject = ["$location", "$window", "$filter", "$timeout", "routingService", "cssFrameworkService", "SubmissionsService", "SchemasService"];
2826
- function recordHandler($location, $window, $filter, $timeout, routingService, cssFrameworkService, 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) {
2827
3264
  // TODO: Put this in a service
2828
3265
  var makeMongoId = function (rnd) {
2829
3266
  if (rnd === void 0) { rnd = function (r16) { return Math.floor(r16).toString(16); }; }
@@ -3076,18 +3513,12 @@ var fng;
3076
3513
  var idList = $scope[suffixCleanId(schemaEntry, "_ids")];
3077
3514
  var thisConversion = void 0;
3078
3515
  if (fieldValue && idList && idList.length > 0) {
3079
- if (fieldName.indexOf(".") !== -1) {
3080
- throw new Error("Trying to directly assign to a nested field 332");
3081
- } // Not sure that this can happen, but put in a runtime test
3082
3516
  if (
3083
- /*
3084
- Check we are starting with an ObjectId (ie not being called because of $watch on conversion, with a
3085
- converted value, which would cause an exception)
3086
- */
3087
- fieldValue.toString().match(/^[a-f0-9]{24}$/) &&
3088
- /*
3089
- We are not suppressing conversions
3090
- */
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
3091
3522
  (!schemaEntry.internalRef || !schemaEntry.internalRef.noConvert)) {
3092
3523
  anObject[fieldName] = convertForeignKeys(schemaEntry, fieldValue, $scope[suffixCleanId(schemaEntry, "Options")], idList);
3093
3524
  }
@@ -3436,7 +3867,7 @@ var fng;
3436
3867
  }
3437
3868
  function handleError($scope) {
3438
3869
  return function (response) {
3439
- if ([200, 400].indexOf(response.status) !== -1) {
3870
+ if ([200, 400, 403].indexOf(response.status) !== -1) {
3440
3871
  var errorMessage = "";
3441
3872
  if (response.data && response.data.errors) {
3442
3873
  for (var errorField in response.data.errors) {
@@ -3460,6 +3891,9 @@ var fng;
3460
3891
  else {
3461
3892
  errorMessage = response.data.message || response.data._message || response.data.err || "Error! Sorry - No further details available.";
3462
3893
  }
3894
+ // anyone using a watch on $scope.phase, and waiting for it to become "ready" before proceeding, will probably
3895
+ // want to know that an error has occurred. This value is NOT used anywhere in forms-angular.
3896
+ $scope.phase = "error";
3463
3897
  $scope.showError(errorMessage);
3464
3898
  }
3465
3899
  else {
@@ -3522,12 +3956,22 @@ var fng;
3522
3956
  find: $location.$$search.f,
3523
3957
  limit: $scope.pageSize,
3524
3958
  skip: pagesLoaded * $scope.pageSize,
3525
- order: $location.$$search.o
3959
+ order: $location.$$search.o,
3960
+ concatenate: false
3526
3961
  })
3527
3962
  .then(function (response) {
3528
3963
  var data = response.data;
3529
3964
  if (angular.isArray(data)) {
3530
- // I have seen an intermittent problem where a page is requested twice
3965
+ // if the options for the resource identified by $scope.modelName has disambiguation parameters,
3966
+ // and that resource has more than one list field, the items returned by getPagedAndFilteredList
3967
+ // might include a "disambiguation" property. for this to appear on the list page, we need
3968
+ // to add an item for it to the list schema
3969
+ if (!$scope.listSchema.find(function (f) { return f.name === "disambiguation"; }) && data.some(function (d) { return d.disambiguation; })) {
3970
+ $scope.listSchema.push({
3971
+ name: "disambiguation",
3972
+ });
3973
+ }
3974
+ // I have seen an intermittent problem where a page is requested twice
3531
3975
  if (pagesLoaded === $scope.pagesLoaded) {
3532
3976
  $scope.pagesLoaded++;
3533
3977
  $scope.recordList = $scope.recordList.concat(data);
@@ -3606,50 +4050,47 @@ var fng;
3606
4050
  },
3607
4051
  getListData: getListData,
3608
4052
  suffixCleanId: suffixCleanId,
4053
+ getData: getData,
3609
4054
  setData: setData,
3610
4055
  setUpLookupOptions: function setUpLookupOptions(lookupCollection, schemaElement, $scope, ctrlState, handleSchema) {
3611
4056
  var optionsList = $scope[schemaElement.options] = [];
3612
4057
  var idList = $scope[schemaElement.ids] = [];
3613
- SchemasService.getSchema(lookupCollection)
4058
+ var dataRequest = !!schemaElement.filter
4059
+ ? 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
4060
+ : SubmissionsService.getAllListAttributes(lookupCollection);
4061
+ dataRequest
3614
4062
  .then(function (response) {
3615
- var data = response.data;
3616
- var listInstructions = [];
3617
- handleSchema("Lookup " + lookupCollection, data, null, listInstructions, "", false, $scope, ctrlState);
3618
- var dataRequest;
3619
- if (typeof schemaElement.filter !== "undefined" && schemaElement.filter) {
3620
- dataRequest = SubmissionsService.getPagedAndFilteredList(lookupCollection, schemaElement.filter);
3621
- }
3622
- else {
3623
- dataRequest = SubmissionsService.getAll(lookupCollection);
3624
- }
3625
- dataRequest
3626
- .then(function (response) {
3627
- var data = angular.copy(response.data);
3628
- if (data) {
3629
- for (var i = 0; i < data.length; i++) {
3630
- var option = "";
3631
- for (var j = 0; j < listInstructions.length; j++) {
3632
- var thisVal = data[i][listInstructions[j].name];
3633
- option += thisVal ? thisVal + " " : "";
3634
- }
3635
- option = option.trim();
3636
- var pos = _.sortedIndex(optionsList, option);
3637
- // handle dupes (ideally people will use unique indexes to stop them but...)
3638
- if (optionsList[pos] === option) {
3639
- option = option + " (" + data[i]._id + ")";
3640
- pos = _.sortedIndex(optionsList, option);
4063
+ var items = response.data;
4064
+ if (items) {
4065
+ items.sort(function (a, b) { return a.text.localeCompare(b.text); });
4066
+ optionsList.push.apply(optionsList, items.map(function (i) { return i.text; }));
4067
+ idList.push.apply(idList, items.map(function (i) { return i.id; }));
4068
+ var dupes = new Set();
4069
+ for (var i = 0; i < optionsList.length - 1; i++) {
4070
+ for (var j = i + 1; j < optionsList.length; j++) {
4071
+ if (_.isEqual(optionsList[i], optionsList[j])) {
4072
+ dupes.add(optionsList[i]);
3641
4073
  }
3642
- optionsList.splice(pos, 0, option);
3643
- idList.splice(pos, 0, data[i]._id);
3644
4074
  }
3645
- if ($scope.readingRecord) {
3646
- $scope.readingRecord
3647
- .then(function () {
3648
- updateRecordWithLookupValues(schemaElement, $scope, ctrlState);
3649
- });
4075
+ }
4076
+ // append the id to any duplicates to make them unique
4077
+ dupes.forEach(function (d) {
4078
+ for (var i = 0; i < optionsList.length; i++) {
4079
+ if (optionsList[i] === d) {
4080
+ optionsList[i] += "(" + idList[i] + ")";
4081
+ }
3650
4082
  }
4083
+ });
4084
+ if ($scope.readingRecord) {
4085
+ $scope.readingRecord
4086
+ .then(function () {
4087
+ updateRecordWithLookupValues(schemaElement, $scope, ctrlState);
4088
+ });
3651
4089
  }
3652
- });
4090
+ }
4091
+ })
4092
+ .catch(function (e) {
4093
+ $scope.handleHttpError(e);
3653
4094
  });
3654
4095
  },
3655
4096
  setUpLookupListOptions: function setUpLookupListOptions(ref, formInstructions, $scope, ctrlState) {
@@ -3771,7 +4212,7 @@ var fng;
3771
4212
  $scope.showError = function (error, alertTitle) {
3772
4213
  $scope.alertTitle = alertTitle ? alertTitle : "Error!";
3773
4214
  if (typeof error === "string") {
3774
- $scope.errorMessage = error;
4215
+ $scope.errorMessage = $sce.trustAsHtml(error);
3775
4216
  }
3776
4217
  else if (!error) {
3777
4218
  $scope.errorMessage = "An error occurred - that's all we got. Sorry.";
@@ -3945,10 +4386,11 @@ var fng;
3945
4386
  }
3946
4387
  };
3947
4388
  $scope.isCancelDisabled = function () {
4389
+ var _a;
3948
4390
  if ($scope[$scope.topLevelFormName] && $scope[$scope.topLevelFormName].$pristine) {
3949
4391
  return true;
3950
4392
  }
3951
- else if (typeof $scope.disableFunctions.isCancelDisabled === "function") {
4393
+ else if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isCancelDisabled) === "function") {
3952
4394
  return $scope.disableFunctions.isCancelDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
3953
4395
  }
3954
4396
  else {
@@ -3956,6 +4398,7 @@ var fng;
3956
4398
  }
3957
4399
  };
3958
4400
  $scope.isSaveDisabled = function () {
4401
+ var _a;
3959
4402
  $scope.whyDisabled = undefined;
3960
4403
  var pristine = false;
3961
4404
  function generateWhyDisabledMessage(form, subFormName) {
@@ -4028,7 +4471,7 @@ var fng;
4028
4471
  if (pristine || !!$scope.whyDisabled) {
4029
4472
  return true;
4030
4473
  }
4031
- else if (typeof $scope.disableFunctions.isSaveDisabled !== "function") {
4474
+ else if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isSaveDisabled) !== "function") {
4032
4475
  return false;
4033
4476
  }
4034
4477
  else {
@@ -4043,10 +4486,11 @@ var fng;
4043
4486
  }
4044
4487
  };
4045
4488
  $scope.isDeleteDisabled = function () {
4489
+ var _a;
4046
4490
  if (!$scope.id) {
4047
4491
  return true;
4048
4492
  }
4049
- else if (typeof $scope.disableFunctions.isDeleteDisabled === "function") {
4493
+ else if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isDeleteDisabled) === "function") {
4050
4494
  return $scope.disableFunctions.isDeleteDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
4051
4495
  }
4052
4496
  else {
@@ -4054,7 +4498,8 @@ var fng;
4054
4498
  }
4055
4499
  };
4056
4500
  $scope.isNewDisabled = function () {
4057
- if (typeof $scope.disableFunctions.isNewDisabled === "function") {
4501
+ var _a;
4502
+ if (typeof ((_a = $scope.disableFunctions) === null || _a === void 0 ? void 0 : _a.isNewDisabled) === "function") {
4058
4503
  return $scope.disableFunctions.isNewDisabled($scope.record, ctrlState.master, $scope[$scope.topLevelFormName]);
4059
4504
  }
4060
4505
  else {
@@ -4090,11 +4535,25 @@ var fng;
4090
4535
  //}
4091
4536
  };
4092
4537
  $scope.sortableOptions = {
4093
- update: function () {
4094
- if ($scope.topLevelFormName) {
4538
+ update: function (e, ui) {
4539
+ if (e.target.hasAttribute("disabled")) {
4540
+ // where formsAngular.elemSecurityFuncBinding is set to "one-time" or "normal", the <ol> that the
4541
+ // ui-sortable directive has been used with will have an ng-disabled that may or may not have caused
4542
+ // a disabled attribute to be added to that element. in the case where this attribute has been
4543
+ // added, sorting should be prevented.
4544
+ // allowing the user to begin the drag, and then preventing it only once they release the mouse button,
4545
+ // doesn't seem like the best solution, but I've yet to find something that works better. the
4546
+ // cancel property (see commented-out code below) looks like it should work (and kind of does), but this
4547
+ // screws up mouse events on input fields hosted within the draggable <li> items, so you're
4548
+ // basically prevented from updating any form element in the nested schema
4549
+ ui.item.sortable.cancel();
4550
+ }
4551
+ else if ($scope.topLevelFormName) {
4095
4552
  $scope[$scope.topLevelFormName].$setDirty();
4096
4553
  }
4097
- }
4554
+ },
4555
+ // don't do this (see comment above)
4556
+ //cancel: "ol[disabled]>li"
4098
4557
  };
4099
4558
  $scope.setUpCustomLookupOptions = function (schemaElement, ids, options, baseScope) {
4100
4559
  for (var _i = 0, _a = [$scope, baseScope]; _i < _a.length; _i++) {
@@ -4145,6 +4604,222 @@ var fng;
4145
4604
  })(services = fng.services || (fng.services = {}));
4146
4605
  })(fng || (fng = {}));
4147
4606
  /// <reference path="../../../../node_modules/@types/angular/index.d.ts" />
4607
+ var fng;
4608
+ (function (fng) {
4609
+ var services;
4610
+ (function (services) {
4611
+ /*@ngInject*/
4612
+ securityService.$inject = ["$rootScope"];
4613
+ function securityService($rootScope) {
4614
+ function canDoSecurity(type) {
4615
+ return (!!fng.formsAngular.elemSecurityFuncBinding &&
4616
+ ((type === "hidden" && !!fng.formsAngular.hiddenSecurityFuncName) ||
4617
+ (type === "disabled" && !!fng.formsAngular.disabledSecurityFuncName)));
4618
+ }
4619
+ function canDoSecurityNow(scope, type) {
4620
+ return (canDoSecurity(type) && // we have security configured
4621
+ (
4622
+ // the host app has not (temporarily) disabled this security type (which it might do, as an optimisation, when there are
4623
+ // currently no security rules to apply); and
4624
+ // it has provided the callbacks that are specified in the security configuration; and
4625
+ // the provided scope (if any) has been decorated (by us). pages and popups which aren't form controllers will need to use
4626
+ // (either directly, or through formMarkupHelper), the decorateSecurableScope() function below
4627
+ (type === "hidden" &&
4628
+ $rootScope[fng.formsAngular.hiddenSecurityFuncName] &&
4629
+ (!scope || !!scope.isSecurelyHidden))
4630
+ ||
4631
+ (type === "disabled" &&
4632
+ $rootScope[fng.formsAngular.disabledSecurityFuncName] &&
4633
+ (!scope || !!scope.isSecurelyDisabled))));
4634
+ }
4635
+ function isSecurelyHidden(elemId, pseudoUrl) {
4636
+ return $rootScope[fng.formsAngular.hiddenSecurityFuncName](elemId, pseudoUrl);
4637
+ }
4638
+ function getSecureDisabledState(elemId, pseudoUrl) {
4639
+ return $rootScope[fng.formsAngular.disabledSecurityFuncName](elemId, pseudoUrl);
4640
+ }
4641
+ function isSecurelyDisabled(elemId, pseudoUrl) {
4642
+ return !!getSecureDisabledState(elemId, pseudoUrl); // either true or "+"
4643
+ }
4644
+ function getBindingStr() {
4645
+ return fng.formsAngular.elemSecurityFuncBinding === "one-time" ? "::" : "";
4646
+ }
4647
+ function ignoreElemId(elemId) {
4648
+ var _a;
4649
+ return (_a = fng.formsAngular.ignoreIdsForHideableOrDisableableAttrs) === null || _a === void 0 ? void 0 : _a.some(function (id) { return elemId.includes(id); });
4650
+ }
4651
+ function getXableAttrs(elemId, attr) {
4652
+ if (elemId && attr && !ignoreElemId(elemId)) {
4653
+ return " ".concat(attr, " title=\"").concat(elemId, "\"");
4654
+ }
4655
+ else {
4656
+ return "";
4657
+ }
4658
+ }
4659
+ function getDisableableAttrs(elemId) {
4660
+ // even when an element should not actually be disabled, we should still mark what would otherwise have been a
4661
+ // potentially-disabled element with scope.disableableAttr - where this is set - and where it is set, also set its
4662
+ // title to be the same as its id so that users can learn of its id by hovering over it. this will
4663
+ // help anyone trying to figure out what is the right element id to use for a DOM security rule
4664
+ return getXableAttrs(elemId, fng.formsAngular.disableableAttr);
4665
+ }
4666
+ function getDisableableAncestorAttrs(elemId) {
4667
+ // even when an element should not actually be disabled, we should still mark what would otherwise have been a
4668
+ // potentially-disabled element with scope.disableableAttr - where this is set - and where it is set, also set its
4669
+ // title to be the same as its id so that users can learn of its id by hovering over it. this will
4670
+ // help anyone trying to figure out what is the right element id to use for a DOM security rule
4671
+ return getXableAttrs(elemId, fng.formsAngular.disableableAncestorAttr);
4672
+ }
4673
+ function getHideableAttrs(elemId) {
4674
+ // even when canDoSecurityNow() returns false, we should still mark what would otherwise have been a
4675
+ // potentially-hidden element with scope.hideableAttr, where this is set, and where it is set, also set its
4676
+ // title to be the same as its id so that users can learn of its id by hovering over it. this will
4677
+ // help anyone trying to figure out what is the right element id to use for a DOM security rule
4678
+ return getXableAttrs(elemId, fng.formsAngular.hideableAttr);
4679
+ }
4680
+ return {
4681
+ canDoSecurity: canDoSecurity,
4682
+ canDoSecurityNow: canDoSecurityNow,
4683
+ isSecurelyHidden: isSecurelyHidden,
4684
+ isSecurelyDisabled: isSecurelyDisabled,
4685
+ getHideableAttrs: getHideableAttrs,
4686
+ getDisableableAttrs: getDisableableAttrs,
4687
+ getDisableableAncestorAttrs: getDisableableAncestorAttrs,
4688
+ // whilst initialising new pages and popups, pass their scope here for decoration with functions that can be used to check
4689
+ // the disabled / hidden state of DOM elements on that page according to the prevailing security rules.
4690
+ // if the host app indicates that security checks should be skipped for this page, we will NOT assign the corresponding
4691
+ // functions. the presence of these functions will be checked later by canDoSecurityNow(), which will always be called
4692
+ // before any security logic is applied. this allows security to be bypassed entirely at the request of the host app,
4693
+ // providing an opportunity for optimisation.
4694
+ decorateSecurableScope: function (securableScope, params) {
4695
+ if (canDoSecurity("hidden") && (!fng.formsAngular.skipHiddenSecurityFuncName || (params === null || params === void 0 ? void 0 : params.overrideSkipping) || !$rootScope[fng.formsAngular.skipHiddenSecurityFuncName](params === null || params === void 0 ? void 0 : params.pseudoUrl))) {
4696
+ securableScope.isSecurelyHidden = function (elemId) {
4697
+ return isSecurelyHidden(elemId, params === null || params === void 0 ? void 0 : params.pseudoUrl);
4698
+ };
4699
+ }
4700
+ if (canDoSecurity("disabled") && (!fng.formsAngular.skipDisabledSecurityFuncName || (params === null || params === void 0 ? void 0 : params.overrideSkipping) || !$rootScope[fng.formsAngular.skipDisabledSecurityFuncName](params === null || params === void 0 ? void 0 : params.pseudoUrl))) {
4701
+ securableScope.isSecurelyDisabled = function (elemId) {
4702
+ return isSecurelyDisabled(elemId, params === null || params === void 0 ? void 0 : params.pseudoUrl);
4703
+ };
4704
+ if (!fng.formsAngular.skipDisabledAncestorSecurityFuncName || (params === null || params === void 0 ? void 0 : params.overrideSkipping) || !$rootScope[fng.formsAngular.skipDisabledAncestorSecurityFuncName](params === null || params === void 0 ? void 0 : params.pseudoUrl)) {
4705
+ securableScope.requiresDisabledChildren = function (elemId) {
4706
+ return getSecureDisabledState(elemId, params === null || params === void 0 ? void 0 : params.pseudoUrl) === "+";
4707
+ };
4708
+ }
4709
+ }
4710
+ },
4711
+ doSecurityWhenReady: function (cb) {
4712
+ if (canDoSecurityNow(undefined, "hidden")) {
4713
+ cb();
4714
+ }
4715
+ else if (canDoSecurity("hidden")) {
4716
+ // wait until the hidden security function has been provided (externally) before proceeding with the callback...
4717
+ // we assume here that the hidden security and disabled security functions are both going to be provided at the
4718
+ // same time (and could therefore watch for either of these things)
4719
+ var unwatch_2 = $rootScope.$watch(fng.formsAngular.hiddenSecurityFuncName, function (newValue) {
4720
+ if (newValue) {
4721
+ unwatch_2();
4722
+ cb();
4723
+ }
4724
+ });
4725
+ }
4726
+ },
4727
+ considerVisibility: function (id, scope) {
4728
+ var hideableAttrs = getHideableAttrs(id);
4729
+ if (canDoSecurityNow(scope, "hidden")) {
4730
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
4731
+ if (scope.isSecurelyHidden(id)) {
4732
+ // if our securityFunc supports instant binding and evaluates to true, then nothing needs to be
4733
+ // added to the dom for this field, which we indicate to our caller as follows...
4734
+ return { omit: true };
4735
+ }
4736
+ }
4737
+ else {
4738
+ return { visibilityAttr: "data-ng-if=\"".concat(getBindingStr(), "!isSecurelyHidden('").concat(id, "')\"").concat(hideableAttrs) };
4739
+ }
4740
+ }
4741
+ return { visibilityAttr: hideableAttrs };
4742
+ },
4743
+ // consider the visibility of a container whose visibility depends upon at least some of its content being visible.
4744
+ // the container is assumed not to itself be securable (hence it doesn't have an id - or at least, not one we're
4745
+ // concerned about - and it doesn't itself need a "hideable" attribute)
4746
+ considerContainerVisibility: function (contentIds, scope) {
4747
+ if (canDoSecurityNow(scope, "hidden")) {
4748
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
4749
+ if (contentIds.some(function (id) { return !scope.isSecurelyHidden(id); })) {
4750
+ return {};
4751
+ }
4752
+ else {
4753
+ return { omit: true };
4754
+ }
4755
+ }
4756
+ else {
4757
+ var attrs = contentIds.map(function (id) { return "!isSecurelyHidden('".concat(id, "')"); });
4758
+ return { visibilityAttr: "data-ng-if=\"".concat(getBindingStr(), "(").concat(attrs.join(" || "), ")\"") };
4759
+ }
4760
+ }
4761
+ return {};
4762
+ },
4763
+ // Generate an attribute that could be added to the element with the given id to enable or disable it
4764
+ // according to the prevailing security rules. In most cases, the attribute will either be "disabled"
4765
+ // (in the case of 'instant' binding) or data-ng-disabled="xxxx" (in the case of one-time or normal
4766
+ // binding). For directives that require something different, use params:
4767
+ // - forceNg will wrap a positive (disabled) result in an angular directive (e.g., data-ng-disabled="true")
4768
+ // rather than returning simply "disabled"
4769
+ // - attrRequiresValue will translate a positive (disabled) result into an attribute with a truthy value
4770
+ // (e.g., disabled="true") rather than returning simply "disabled"
4771
+ // - attr can be used in the case where a directive expects an attribute other than "disabled".
4772
+ // (for example, uib-tab expects "disable").
4773
+ // Even if the element is not be disabled on this occasion, we will always return the value of
4774
+ // fng.formsAngular.disableableAttr - where set - as a way of marking it as potentially disableable.
4775
+ // Because they can also have a readonly attribute which needs to be taken into consideration, this
4776
+ // function is NOT suitable for fields, which are instead handled by fieldformMarkupHelper.handleReadOnlyDisabled().
4777
+ generateDisabledAttr: function (id, scope, params) {
4778
+ function getActuallyDisabledAttrs() {
4779
+ var result = "";
4780
+ if (canDoSecurityNow(scope, "disabled")) {
4781
+ if (!params) {
4782
+ params = {};
4783
+ }
4784
+ if (params.attrRequiresValue && params.forceNg) {
4785
+ throw new Error("Invalid combination of parameters provided to generateDisabledAttr() [attrRequiresValue and forceNg]");
4786
+ }
4787
+ var attr = params.attr || "disabled";
4788
+ if (fng.formsAngular.elemSecurityFuncBinding === "instant") {
4789
+ if (scope.isSecurelyDisabled(id)) {
4790
+ if (params.attrRequiresValue) {
4791
+ return " ".concat(attr, "=\"true\"");
4792
+ }
4793
+ else if (params.forceNg) {
4794
+ result = "true";
4795
+ }
4796
+ else {
4797
+ return " ".concat(attr);
4798
+ }
4799
+ }
4800
+ }
4801
+ else {
4802
+ result = "".concat(getBindingStr(), "isSecurelyDisabled('").concat(id, "')");
4803
+ }
4804
+ if (result) {
4805
+ if (attr === "disabled") {
4806
+ return " data-ng-disabled=\"".concat(result, "\"");
4807
+ }
4808
+ else {
4809
+ return " data-ng-attr-".concat(attr, "=\"").concat(result, "\"");
4810
+ }
4811
+ }
4812
+ }
4813
+ return result;
4814
+ }
4815
+ return getActuallyDisabledAttrs() + getDisableableAttrs(id);
4816
+ },
4817
+ };
4818
+ }
4819
+ services.securityService = securityService;
4820
+ })(services = fng.services || (fng.services = {}));
4821
+ })(fng || (fng = {}));
4822
+ /// <reference path="../../../../node_modules/@types/angular/index.d.ts" />
4148
4823
  var ExpirationCache = /** @class */ (function () {
4149
4824
  function ExpirationCache(timeout) {
4150
4825
  if (timeout === void 0) { timeout = 60 * 1000; }
@@ -4190,9 +4865,11 @@ var fng;
4190
4865
  {
4191
4866
  aggregate - whether or not to aggregate results (http://docs.mongodb.org/manual/aggregation/)
4192
4867
  find - find parameter
4868
+ projection - the fields to return
4193
4869
  limit - limit results to this number of records
4194
4870
  skip - skip this number of records before returning results
4195
4871
  order - sort order
4872
+ 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)
4196
4873
  }
4197
4874
  */
4198
4875
  var generateListQuery = function (options) {
@@ -4213,9 +4890,11 @@ var fng;
4213
4890
  };
4214
4891
  addParameter('l', options.limit);
4215
4892
  addParameter('f', options.find);
4893
+ addParameter('p', options.projection);
4216
4894
  addParameter('a', options.aggregate);
4217
4895
  addParameter('o', options.order);
4218
4896
  addParameter('s', options.skip);
4897
+ addParameter('c', options.concatenate);
4219
4898
  return queryString;
4220
4899
  };
4221
4900
  // TODO Figure out tab history updates (check for other tab-history-todos)
@@ -4240,12 +4919,35 @@ var fng;
4240
4919
  // changed: changed
4241
4920
  // };
4242
4921
  // },
4243
- getListAttributes: function (ref, id) {
4922
+ // return only the list attributes for the given record. where returnRaw is true, the record's
4923
+ // list attributes will be returned without transformation. otherwise, the list attributes will be concatenated
4924
+ // (with spaces) and returned in the form { list: string }
4925
+ getListAttributes: function (ref, id, returnRaw) {
4244
4926
  var actualId = typeof id === "string" ? id : id.id || id._id || id.x || id;
4245
- if (typeof actualId !== "string") {
4246
- throw new Error("getListAttributes requires a string id but was provided with ".concat(JSON.stringify(id)));
4927
+ if (typeof actualId === "object") {
4928
+ throw new Error("getListAttributes doesn't expect an object but was provided with ".concat(JSON.stringify(id)));
4929
+ }
4930
+ var queryString = returnRaw ? "?returnRaw=1" : "";
4931
+ return $http.get("/api/".concat(ref, "/").concat(actualId, "/list").concat(queryString), { cache: expCache });
4932
+ },
4933
+ // return only the list attributes for ALL records in the given collection, returning ILookupItem[]
4934
+ getAllListAttributes: function (ref) {
4935
+ return $http.get("/api/".concat(ref, "/listAll"), { cache: expCache });
4936
+ },
4937
+ // return only the list attributes for records in the given collection that satisfy the given query conditions (filter, limit etc.)
4938
+ // return ILookupItem[] if options.concatenate is true, else the raw documents
4939
+ getPagedAndFilteredList: function (ref, options) {
4940
+ if (options.projection) {
4941
+ throw new Error("Cannot use projection option for getPagedAndFilteredList, because it only returns list fields");
4247
4942
  }
4248
- return $http.get('/api/' + ref + '/' + actualId + '/list', { cache: expCache });
4943
+ if (options.concatenate === undefined) {
4944
+ options.concatenate = false;
4945
+ }
4946
+ return $http.get("/api/".concat(ref, "/listAll").concat(generateListQuery(options)));
4947
+ },
4948
+ // return ALL attributes for records in the given collection that satisfy the given query conditions (filter, limit etc.)
4949
+ getPagedAndFilteredListFull: function (ref, options) {
4950
+ return $http.get("/api/".concat(ref).concat(generateListQuery(options)));
4249
4951
  },
4250
4952
  readRecord: function (modelName, id) {
4251
4953
  // TODO Figure out tab history updates (check for other tab-history-todos)
@@ -4253,7 +4955,11 @@ var fng;
4253
4955
  // if (tabChangeData && tabChangeData.model === modelName && tabChangeData.id === id) {
4254
4956
  // retVal = Promise.resolve({data:tabChangeData.record, changed: tabChangeData.changed, master: tabChangeData.master});
4255
4957
  // } else {
4256
- return $http.get('/api/' + modelName + '/' + id);
4958
+ var actualId = typeof id === "string" ? id : id.id || id._id || id.x || id;
4959
+ if (typeof actualId === "object") {
4960
+ throw new Error("readRecord doesn't expect an object but was provided with ".concat(JSON.stringify(id)));
4961
+ }
4962
+ return $http.get("/api/".concat(modelName, "/").concat(actualId));
4257
4963
  // retVal = $http.get('/api/' + modelName + '/' + id);
4258
4964
  // }
4259
4965
  // tabChangeData = null;
@@ -4263,21 +4969,18 @@ var fng;
4263
4969
  var options = angular.extend({
4264
4970
  cache: useCacheForGetAll ? expCache : false
4265
4971
  }, _options);
4266
- return $http.get('/api/' + modelName, options);
4267
- },
4268
- getPagedAndFilteredList: function (modelName, options) {
4269
- return $http.get('/api/' + modelName + generateListQuery(options));
4972
+ return $http.get("/api/".concat(modelName), options);
4270
4973
  },
4271
4974
  deleteRecord: function (model, id) {
4272
- return $http.delete('/api/' + model + '/' + id);
4975
+ return $http.delete("/api/".concat(model, "/").concat(id));
4273
4976
  },
4274
4977
  updateRecord: function (modelName, id, dataToSave) {
4275
- expCache.remove('/api/' + modelName);
4276
- return $http.post('/api/' + modelName + '/' + id, dataToSave);
4978
+ expCache.remove("/api/".concat(modelName));
4979
+ return $http.post("/api/".concat(modelName, "/").concat(id), dataToSave);
4277
4980
  },
4278
4981
  createRecord: function (modelName, dataToSave) {
4279
- expCache.remove('/api/' + modelName);
4280
- return $http.post('/api/' + modelName, dataToSave);
4982
+ expCache.remove("/api/".concat(modelName));
4983
+ return $http.post("/api/".concat(modelName), dataToSave);
4281
4984
  },
4282
4985
  useCache: function (val) {
4283
4986
  useCacheForGetAll = val;
@@ -4409,11 +5112,14 @@ var fng;
4409
5112
  var controllers;
4410
5113
  (function (controllers) {
4411
5114
  /*@ngInject*/
4412
- NavCtrl.$inject = ["$rootScope", "$window", "$scope", "$location", "$filter", "routingService", "cssFrameworkService"];
4413
- function NavCtrl($rootScope, $window, $scope, $location, $filter, routingService, cssFrameworkService) {
5115
+ NavCtrl.$inject = ["$rootScope", "$window", "$scope", "$filter", "routingService", "cssFrameworkService", "securityService"];
5116
+ function NavCtrl($rootScope, $window, $scope, $filter, routingService, cssFrameworkService, securityService) {
4414
5117
  function clearContextMenu() {
4415
5118
  $scope.items = [];
4416
5119
  $scope.contextMenu = undefined;
5120
+ $scope.contextMenuId = undefined;
5121
+ $scope.contextMenuHidden = undefined;
5122
+ $scope.contextMenuDisabled = undefined;
4417
5123
  }
4418
5124
  $rootScope.navScope = $scope; // Lets plugins access menus
4419
5125
  clearContextMenu();
@@ -4491,11 +5197,25 @@ var fng;
4491
5197
  }
4492
5198
  return result;
4493
5199
  };
5200
+ function initialiseContextMenu(menuCaption) {
5201
+ $scope.contextMenu = menuCaption;
5202
+ var menuId = "".concat(_.camelCase(menuCaption), "ContextMenu");
5203
+ // the context menu itself (see dropdown.ts) has an ng-if that checks for a value of
5204
+ // contextMenuId. let's delete this until we know we're ready to evaluate the security
5205
+ // of the menu items...
5206
+ $scope.contextMenuId = undefined;
5207
+ securityService.doSecurityWhenReady(function () {
5208
+ //... which we now are
5209
+ $scope.contextMenuId = menuId;
5210
+ $scope.contextMenuHidden = securityService.isSecurelyHidden($scope.contextMenuId);
5211
+ $scope.contextMenuDisabled = securityService.isSecurelyDisabled($scope.contextMenuId);
5212
+ });
5213
+ }
4494
5214
  $scope.$on('fngControllersLoaded', function (evt, sharedData, modelName) {
4495
- $scope.contextMenu = sharedData.dropDownDisplay || sharedData.modelNameDisplay || $filter('titleCase')(modelName, false);
5215
+ initialiseContextMenu(sharedData.dropDownDisplay || sharedData.modelNameDisplay || $filter('titleCase')(modelName, false));
4496
5216
  if (sharedData.dropDownDisplayPromise) {
4497
5217
  sharedData.dropDownDisplayPromise.then(function (value) {
4498
- $scope.contextMenu = value;
5218
+ initialiseContextMenu(value);
4499
5219
  });
4500
5220
  }
4501
5221
  });
@@ -4544,8 +5264,9 @@ var fng;
4544
5264
  return item.isHidden ? item.isHidden() : false;
4545
5265
  }
4546
5266
  var dividerHide = false;
5267
+ var item = $scope.items[index];
4547
5268
  // Hide a divider if it appears under another
4548
- if ($scope.items[index].divider) {
5269
+ if (item.divider) {
4549
5270
  if (index === 0) {
4550
5271
  dividerHide = true;
4551
5272
  }
@@ -4565,7 +5286,7 @@ var fng;
4565
5286
  }
4566
5287
  }
4567
5288
  }
4568
- return dividerHide || explicitlyHidden($scope.items[index]);
5289
+ return dividerHide || explicitlyHidden(item);
4569
5290
  };
4570
5291
  $scope.isDisabled = function (index) {
4571
5292
  return $scope.items[index].isDisabled ? $scope.items[index].isDisabled() : false;
@@ -4651,7 +5372,8 @@ var fng;
4651
5372
  .factory('pluginHelper', fng.services.pluginHelper)
4652
5373
  .factory('recordHandler', fng.services.recordHandler)
4653
5374
  .factory('SchemasService', fng.services.SchemasService)
4654
- .factory('SubmissionsService', fng.services.SubmissionsService);
5375
+ .factory('SubmissionsService', fng.services.SubmissionsService)
5376
+ .factory('securityService', fng.services.securityService);
4655
5377
  })(fng || (fng = {}));
4656
5378
  // expose the library
4657
5379
  var formsAngular = fng.formsAngular;