13a4bbe5ad3194ba10376d8c35b101b9b29d77f7
1 // https://civicrm.org/licensing
2 (function(angular
, $, _
) {
5 function backfillEntityDefaults(entity
) {
6 // These fields did not exist in prior versions. In absence of explicit, these are the values inferred by runtime/server-side.
7 // We cannot currently backfill schema in the upgrade, so this is the next best opportunity.
8 if (entity
.actions
=== undefined) entity
.actions
= {create
: true, update
: true};
9 if (entity
.security
=== undefined) entity
.security
= 'RBAC';
13 angular
.module('afGuiEditor').component('afGuiEditor', {
14 templateUrl
: '~/afGuiEditor/afGuiEditor.html',
20 controllerAs
: 'editor',
21 controller: function($scope
, crmApi4
, afGui
, $parse
, $timeout
, $location
) {
22 var ts
= $scope
.ts
= CRM
.ts('org.civicrm.afform_admin');
25 $scope
.saving
= false;
26 $scope
.selectedEntityName
= null;
27 this.meta
= afGui
.meta
;
31 this.$onInit = function() {
32 // Load the current form plus blocks & fields
34 afGui
.addMeta(this.data
);
37 $timeout(fixEditorHeight
);
38 $timeout(editor
.adjustTabWidths
);
40 .on('resize.afGuiEditor', fixEditorHeight
)
41 .on('resize.afGuiEditor', editor
.adjustTabWidths
);
44 this.$onDestroy = function() {
45 $(window
).off('.afGuiEditor');
48 // Initialize the current form
49 function initializeForm() {
50 editor
.afform
= editor
.data
.definition
;
52 alert('Error: unknown form');
54 if (editor
.mode
=== 'clone') {
55 delete editor
.afform
.name
;
56 delete editor
.afform
.server_route
;
57 editor
.afform
.is_dashlet
= false;
58 editor
.afform
.title
+= ' ' + ts('(copy)');
60 $scope
.canvasTab
= 'layout';
61 $scope
.layoutHtml
= '';
62 editor
.layout
= {'#children': []};
65 if (editor
.getFormType() === 'form') {
66 editor
.allowEntityConfig
= true;
67 editor
.layout
['#children'] = afGui
.findRecursive(editor
.afform
.layout
, {'#tag': 'af-form'})[0]['#children'];
68 $scope
.entities
= _
.mapValues(afGui
.findRecursive(editor
.layout
['#children'], {'#tag': 'af-entity'}, 'name'), backfillEntityDefaults
);
70 if (editor
.mode
=== 'create') {
71 editor
.addEntity(editor
.entity
);
72 editor
.layout
['#children'].push(afGui
.meta
.elements
.submit
.element
);
76 else if (editor
.getFormType() === 'block') {
77 editor
.layout
['#children'] = editor
.afform
.layout
;
78 editor
.blockEntity
= editor
.afform
.join
|| editor
.afform
.block
;
79 $scope
.entities
[editor
.blockEntity
] = backfillEntityDefaults({
80 type
: editor
.blockEntity
,
81 name
: editor
.blockEntity
,
82 label
: afGui
.getEntity(editor
.blockEntity
).label
86 else if (editor
.getFormType() === 'search') {
87 editor
.layout
['#children'] = afGui
.findRecursive(editor
.afform
.layout
, {'af-fieldset': ''})[0]['#children'];
88 editor
.searchDisplay
= afGui
.findRecursive(editor
.layout
['#children'], function(item
) {
89 return item
['#tag'] && item
['#tag'].indexOf('crm-search-display-') === 0;
91 editor
.searchFilters
= getSearchFilterOptions();
94 // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
95 $scope
.changesSaved
= editor
.mode
=== 'edit' ? 1 : false;
96 $scope
.$watch('editor.afform', function () {
97 $scope
.changesSaved
= $scope
.changesSaved
=== 1;
101 this.getFormType = function() {
102 return editor
.afform
.type
;
105 $scope
.updateLayoutHtml = function() {
106 $scope
.layoutHtml
= '...Loading...';
107 crmApi4('Afform', 'convert', {layout
: editor
.afform
.layout
, from: 'deep', to
: 'html', formatWhitespace
: true})
109 $scope
.layoutHtml
= r
[0].layout
|| '(Error)';
112 $scope
.layoutHtml
= '(Error)';
116 this.addEntity = function(type
, selectTab
) {
117 var meta
= afGui
.meta
.entities
[type
],
119 // Give this new entity a unique name
120 while (!!$scope
.entities
[type
+ num
]) {
123 $scope
.entities
[type
+ num
] = backfillEntityDefaults(_
.assign($parse(meta
.defaults
)($scope
), {
127 label
: meta
.label
+ ' ' + num
,
131 function addToCanvas() {
132 // Add this af-entity tag after the last existing one
133 var pos
= 1 + _
.findLastIndex(editor
.layout
['#children'], {'#tag': 'af-entity'});
134 editor
.layout
['#children'].splice(pos
, 0, $scope
.entities
[type
+ num
]);
135 // Create a new af-fieldset container for the entity
136 var fieldset
= _
.cloneDeep(afGui
.meta
.elements
.fieldset
.element
);
137 fieldset
['af-fieldset'] = type
+ num
;
138 fieldset
['#children'][0]['#children'][0]['#text'] = meta
.label
+ ' ' + num
;
139 // Add boilerplate contents
140 _
.each(meta
.boilerplate
, function (tag
) {
141 fieldset
['#children'].push(tag
);
143 // Attempt to place the new af-fieldset after the last one on the form
144 pos
= 1 + _
.findLastIndex(editor
.layout
['#children'], 'af-fieldset');
146 editor
.layout
['#children'].splice(pos
, 0, fieldset
);
148 editor
.layout
['#children'].push(fieldset
);
150 delete $scope
.entities
[type
+ num
].loading
;
152 editor
.selectEntity(type
+ num
);
153 $timeout(function() {
154 editor
.scrollToEntity(type
+ num
);
157 $timeout(editor
.adjustTabWidths
);
163 $timeout(editor
.adjustTabWidths
);
164 crmApi4('Afform', 'loadAdminData', {
165 definition
: {type
: 'form'},
167 }, 0).then(function(data
) {
174 this.removeEntity = function(entityName
) {
175 delete $scope
.entities
[entityName
];
176 afGui
.removeRecursive(editor
.layout
['#children'], {'#tag': 'af-entity', name
: entityName
});
177 afGui
.removeRecursive(editor
.layout
['#children'], {'af-fieldset': entityName
});
178 this.selectEntity(null);
181 this.selectEntity = function(entityName
) {
182 $scope
.selectedEntityName
= entityName
;
183 $timeout(editor
.adjustTabWidths
);
186 this.getEntity = function(entityName
) {
187 return $scope
.entities
[entityName
];
190 this.getSelectedEntityName = function() {
191 return $scope
.selectedEntityName
;
194 this.getEntityDefn = function(entity
) {
195 if (entity
.type
=== 'Contact' && entity
.data
.contact_type
) {
196 return editor
.meta
.entities
[entity
.data
.contact_type
];
198 return editor
.meta
.entities
[entity
.type
];
201 // Scroll an entity's first fieldset into view of the canvas
202 this.scrollToEntity = function(entityName
) {
203 var $canvas
= $('#afGuiEditor-canvas-body'),
204 $entity
= $('.af-gui-container-type-fieldset[data-entity="' + entityName
+ '"]').first(),
205 // Scrolltop value needed to place entity's fieldset at top of canvas
206 scrollValue
= $canvas
.scrollTop() + ($entity
.offset().top
- $canvas
.offset().top
),
207 // Maximum possible scrollTop (height minus contents height, adjusting for padding)
208 maxScroll
= $('#afGuiEditor-canvas-body > *').height() - $canvas
.height() + 20;
209 // Exceeding the maximum scrollTop breaks the animation so keep it under the limit
210 $canvas
.animate({scrollTop
: scrollValue
> maxScroll
? maxScroll
: scrollValue
}, 500);
213 this.getAfform = function() {
214 return editor
.afform
;
217 this.getEntities = function(filter
) {
218 return filter
? _
.filter($scope
.entities
, filter
) : _
.toArray($scope
.entities
);
221 this.toggleContactSummary = function() {
222 if (editor
.afform
.contact_summary
) {
223 editor
.afform
.contact_summary
= false;
224 if (editor
.afform
.type
=== 'search') {
225 delete editor
.searchDisplay
.filters
;
228 editor
.afform
.contact_summary
= 'block';
229 if (editor
.afform
.type
=== 'search') {
230 editor
.searchDisplay
.filters
= editor
.searchFilters
[0].key
;
235 function getSearchFilterOptions() {
236 var searchDisplay
= editor
.meta
.searchDisplays
[editor
.searchDisplay
['search-name'] + '.' + editor
.searchDisplay
['display-name']],
240 addFields(searchDisplay
['saved_search.api_entity'], '');
242 _
.each(searchDisplay
['saved_search.api_params'].join
, function(join
) {
243 var joinInfo
= join
[0].split(' AS ');
244 addFields(joinInfo
[0], joinInfo
[1] + '.');
247 function addFields(entityName
, prefix
) {
248 var entity
= afGui
.getEntity(entityName
);
249 entityCount
[entity
.entity
] = (entityCount
[entity
.entity
] || 0) + 1;
250 var count
= (entityCount
[entity
.entity
] > 1 ? ' ' + entityCount
[entity
.entity
] : '');
251 if (entityName
=== 'Contact') {
253 key
: "{'" + prefix
+ "id': options.contact_id}",
254 label
: entity
.label
+ count
257 _
.each(entity
.fields
, function(field
) {
258 if (field
.fk_entity
=== 'Contact') {
260 key
: "{'" + prefix
+ field
.name
+ "': options.contact_id}",
261 label
: entity
.label
+ count
+ ' ' + field
.label
270 this.getLink = function() {
271 if (editor
.afform
.server_route
) {
272 return CRM
.url(editor
.afform
.server_route
, null, editor
.afform
.is_public
? 'front' : 'back');
276 // Options for ui-sortable in field palette
277 this.getSortableOptions = function(entityName
) {
278 if (!sortableOptions
[entityName
+ '']) {
279 sortableOptions
[entityName
+ ''] = {
281 appendTo
: '#afGuiEditor-canvas-body > af-gui-container',
282 containment
: '#afGuiEditor-canvas-body',
283 update
: editor
.onDrop
,
284 items
: '> div:not(.disabled)',
285 connectWith
: '#afGuiEditor-canvas ' + (entityName
? '[data-entity="' + entityName
+ '"] > ' : '') + '[ui-sortable]',
286 placeholder
: 'af-gui-dropzone',
287 tolerance
: 'pointer',
291 return sortableOptions
[entityName
+ ''];
294 // Validates that a drag-n-drop action is allowed
295 this.onDrop = function(event
, ui
) {
296 var sort
= ui
.item
.sortable
;
297 // Check if this is a callback for an item dropped into a different container
298 // @see https://github.com/angular-ui/ui-sortable notes on canceling
299 if (!sort
.received
&& sort
.source
[0] !== sort
.droptarget
[0]) {
300 var $source
= $(sort
.source
[0]),
301 $target
= $(sort
.droptarget
[0]),
302 $item
= $(ui
.item
[0]);
303 // Fields cannot be dropped outside their own entity
304 if ($item
.find('af-gui-field').length
) {
305 if ($source
.closest('[data-entity]').attr('data-entity') !== $target
.closest('[data-entity]').attr('data-entity')) {
306 return sort
.cancel();
309 // Entity-fieldsets cannot be dropped into other entity-fieldsets
310 if ((sort
.model
['af-fieldset'] || $item
.find('.af-gui-fieldset').length
) && $target
.closest('.af-gui-fieldset').length
) {
311 return sort
.cancel();
316 $scope
.save = function() {
317 var afform
= JSON
.parse(angular
.toJson(editor
.afform
));
318 // This might be set to undefined by validation
319 afform
.server_route
= afform
.server_route
|| '';
320 $scope
.saving
= $scope
.changesSaved
= true;
321 crmApi4('Afform', 'save', {formatWhitespace
: true, records
: [afform
]})
322 .then(function (data
) {
323 $scope
.saving
= false;
324 editor
.afform
.name
= data
[0].name
;
325 if (editor
.mode
!== 'edit') {
326 $location
.url('/edit/' + data
[0].name
);
331 $scope
.$watch('editor.afform.title', function(newTitle
, oldTitle
) {
332 if (typeof oldTitle
=== 'string') {
333 _
.each($scope
.entities
, function(entity
) {
334 if (entity
.data
&& entity
.data
.source
=== oldTitle
) {
335 entity
.data
.source
= newTitle
;
341 // Force editor panels to a fixed height, to avoid palette scrolling offscreen
342 function fixEditorHeight() {
343 var height
= $(window
).height() - $('#afGuiEditor').offset().top
;
344 $('#afGuiEditor').height(Math
.floor(height
));
347 // Compress tabs on small screens
348 this.adjustTabWidths = function() {
349 $('#afGuiEditor .panel-heading ul.nav-tabs li.active').css('max-width', '');
350 $('#afGuiEditor .panel-heading ul.nav-tabs').each(function() {
351 var remainingSpace
= Math
.floor($(this).width()) - 1,
352 inactiveTabs
= $(this).children('li.fluid-width-tab').not('.active');
353 $(this).children('.active,:not(.fluid-width-tab)').each(function() {
354 remainingSpace
-= $(this).width();
356 if (inactiveTabs
.length
) {
357 inactiveTabs
.css('max-width', Math
.floor(remainingSpace
/ inactiveTabs
.length
) + 'px');
364 })(angular
, CRM
.$, CRM
._
);