Commit | Line | Data |
---|---|---|
b844d2ca CW |
1 | // https://civicrm.org/licensing |
2 | (function(angular, $, _) { | |
3 | "use strict"; | |
4 | ||
3a2ca4f5 TO |
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'; | |
10 | return entity; | |
11 | } | |
12 | ||
b844d2ca CW |
13 | angular.module('afGuiEditor').component('afGuiEditor', { |
14 | templateUrl: '~/afGuiEditor/afGuiEditor.html', | |
15 | bindings: { | |
490565d0 CW |
16 | data: '<', |
17 | entity: '<', | |
18 | mode: '@' | |
b844d2ca CW |
19 | }, |
20 | controllerAs: 'editor', | |
23fd8685 | 21 | controller: function($scope, crmApi4, afGui, $parse, $timeout, $location) { |
67d666c6 | 22 | var ts = $scope.ts = CRM.ts('org.civicrm.afform_admin'); |
2fb9e655 | 23 | |
d617083a | 24 | this.afform = null; |
b844d2ca CW |
25 | $scope.saving = false; |
26 | $scope.selectedEntityName = null; | |
7cc347c2 | 27 | $scope.searchDisplayListFilter = {}; |
23fd8685 | 28 | this.meta = afGui.meta; |
4da60774 | 29 | var editor = this, |
835aeacb CW |
30 | undoHistory = [], |
31 | undoPosition = 0, | |
32 | undoAction = null, | |
1d678002 | 33 | lastSaved, |
4da60774 | 34 | sortableOptions = {}; |
b844d2ca | 35 | |
835aeacb CW |
36 | // ngModelOptions to debounce input |
37 | // Used to prevent cluttering the undo history with every keystroke | |
38 | this.debounceMode = { | |
39 | updateOn: 'default blur', | |
40 | debounce: { | |
41 | default: 2000, | |
42 | blur: 0 | |
43 | } | |
44 | }; | |
45 | ||
46 | // Above mode for use with getterSetter | |
47 | this.debounceWithGetterSetter = _.assign({getterSetter: true}, this.debounceMode); | |
48 | ||
b844d2ca | 49 | this.$onInit = function() { |
490565d0 CW |
50 | // Load the current form plus blocks & fields |
51 | afGui.resetMeta(); | |
52 | afGui.addMeta(this.data); | |
53 | initializeForm(); | |
4da60774 CW |
54 | |
55 | $timeout(fixEditorHeight); | |
56 | $timeout(editor.adjustTabWidths); | |
57 | $(window) | |
835aeacb | 58 | .off('.afGuiEditor') |
4da60774 | 59 | .on('resize.afGuiEditor', fixEditorHeight) |
835aeacb CW |
60 | .on('resize.afGuiEditor', editor.adjustTabWidths) |
61 | .on('keyup.afGuiEditor', editor.onKeyup); | |
62 | ||
63 | // Warn of unsaved changes | |
64 | window.onbeforeunload = function(e) { | |
65 | if (!editor.isSaved()) { | |
66 | e.returnValue = ts("Form has not been saved."); | |
67 | return e.returnValue; | |
68 | } | |
69 | }; | |
4da60774 CW |
70 | }; |
71 | ||
72 | this.$onDestroy = function() { | |
73 | $(window).off('.afGuiEditor'); | |
835aeacb | 74 | window.onbeforeunload = null; |
b844d2ca CW |
75 | }; |
76 | ||
835aeacb CW |
77 | function setEditorLayout() { |
78 | editor.layout = {}; | |
79 | if (editor.getFormType() === 'form') { | |
80 | editor.layout['#children'] = afGui.findRecursive(editor.afform.layout, {'#tag': 'af-form'})[0]['#children']; | |
81 | } | |
82 | else { | |
83 | editor.layout['#children'] = editor.afform.layout; | |
84 | } | |
85 | } | |
86 | ||
b844d2ca | 87 | // Initialize the current form |
490565d0 | 88 | function initializeForm() { |
d617083a CW |
89 | editor.afform = editor.data.definition; |
90 | if (!editor.afform) { | |
490565d0 | 91 | alert('Error: unknown form'); |
b844d2ca | 92 | } |
bc6e7a56 | 93 | if (editor.mode === 'clone') { |
d617083a CW |
94 | delete editor.afform.name; |
95 | delete editor.afform.server_route; | |
96 | editor.afform.is_dashlet = false; | |
97 | editor.afform.title += ' ' + ts('(copy)'); | |
bc6e7a56 | 98 | } |
34c92b2c | 99 | editor.afform.icon = editor.afform.icon || 'fa-list-alt'; |
b844d2ca CW |
100 | $scope.canvasTab = 'layout'; |
101 | $scope.layoutHtml = ''; | |
fbcd8c17 | 102 | $scope.entities = {}; |
835aeacb | 103 | setEditorLayout(); |
1d678002 | 104 | setLastSaved(); |
b844d2ca | 105 | |
7b9f8bdc CW |
106 | if (editor.afform.navigation) { |
107 | loadNavigationMenu(); | |
108 | } | |
109 | ||
d4e2c9ff | 110 | if (editor.getFormType() === 'form') { |
fbcd8c17 | 111 | editor.allowEntityConfig = true; |
3a2ca4f5 | 112 | $scope.entities = _.mapValues(afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name'), backfillEntityDefaults); |
fbcd8c17 CW |
113 | |
114 | if (editor.mode === 'create') { | |
115 | editor.addEntity(editor.entity); | |
e6772fc6 | 116 | editor.afform.create_submission = true; |
fbcd8c17 CW |
117 | editor.layout['#children'].push(afGui.meta.elements.submit.element); |
118 | } | |
119 | } | |
7cc347c2 CW |
120 | |
121 | if (editor.getFormType() === 'block') { | |
736be208 | 122 | editor.blockEntity = editor.afform.join_entity || editor.afform.entity_type || '*'; |
3a2ca4f5 | 123 | $scope.entities[editor.blockEntity] = backfillEntityDefaults({ |
fbcd8c17 CW |
124 | type: editor.blockEntity, |
125 | name: editor.blockEntity, | |
126 | label: afGui.getEntity(editor.blockEntity).label | |
3a2ca4f5 | 127 | }); |
b844d2ca CW |
128 | } |
129 | ||
d4e2c9ff | 130 | else if (editor.getFormType() === 'search') { |
7cc347c2 | 131 | editor.searchDisplays = getSearchDisplaysOnForm(); |
2ef64700 CW |
132 | } |
133 | ||
835aeacb CW |
134 | // Initialize undo history |
135 | undoAction = 'initialLoad'; | |
136 | undoHistory = [{ | |
137 | afform: _.cloneDeep(editor.afform), | |
138 | saved: editor.mode === 'edit', | |
139 | selectedEntityName: null | |
140 | }]; | |
141 | $scope.$watch('editor.afform', function(newValue, oldValue) { | |
142 | if (!undoAction && newValue && oldValue) { | |
143 | // Clear "redo" history | |
144 | if (undoPosition) { | |
145 | undoHistory.splice(0, undoPosition); | |
146 | undoPosition = 0; | |
147 | } | |
148 | undoHistory.unshift({ | |
149 | afform: _.cloneDeep(editor.afform), | |
150 | saved: false, | |
151 | selectedEntityName: $scope.selectedEntityName | |
152 | }); | |
153 | // Trim to a total length of 20 | |
154 | if (undoHistory.length > 20) { | |
155 | undoHistory.splice(20, undoHistory.length - 20); | |
156 | } | |
157 | } | |
158 | undoAction = null; | |
b844d2ca CW |
159 | }, true); |
160 | } | |
161 | ||
835aeacb CW |
162 | // Undo/redo keys (ctrl-z, ctrl-shift-z) |
163 | this.onKeyup = function(e) { | |
164 | if (e.key === 'z' && e.ctrlKey && e.shiftKey) { | |
165 | editor.redo(); | |
166 | } | |
167 | else if (e.key === 'z' && e.ctrlKey) { | |
168 | editor.undo(); | |
169 | } | |
170 | }; | |
171 | ||
172 | this.canUndo = function() { | |
173 | return !!undoHistory[undoPosition + 1]; | |
174 | }; | |
175 | ||
176 | this.canRedo = function() { | |
177 | return !!undoHistory[undoPosition - 1]; | |
178 | }; | |
179 | ||
180 | // Revert to a previous/next revision in the undo history | |
181 | function changeHistory(direction) { | |
182 | if (!undoHistory[undoPosition + direction]) { | |
183 | return; | |
184 | } | |
185 | undoPosition += direction; | |
186 | undoAction = 'change'; | |
187 | editor.afform = _.cloneDeep(undoHistory[undoPosition].afform); | |
188 | setEditorLayout(); | |
189 | $scope.canvasTab = 'layout'; | |
190 | $scope.selectedEntityName = undoHistory[undoPosition].selectedEntityName; | |
191 | } | |
192 | ||
193 | this.undo = _.wrap(1, changeHistory); | |
194 | ||
195 | this.redo = _.wrap(-1, changeHistory); | |
196 | ||
197 | this.isSaved = function() { | |
198 | return undoHistory[undoPosition].saved; | |
199 | }; | |
200 | ||
d4e2c9ff | 201 | this.getFormType = function() { |
d617083a | 202 | return editor.afform.type; |
d4e2c9ff CW |
203 | }; |
204 | ||
b844d2ca CW |
205 | $scope.updateLayoutHtml = function() { |
206 | $scope.layoutHtml = '...Loading...'; | |
d617083a | 207 | crmApi4('Afform', 'convert', {layout: editor.afform.layout, from: 'deep', to: 'html', formatWhitespace: true}) |
b844d2ca CW |
208 | .then(function(r){ |
209 | $scope.layoutHtml = r[0].layout || '(Error)'; | |
210 | }) | |
211 | .catch(function(r){ | |
212 | $scope.layoutHtml = '(Error)'; | |
213 | }); | |
214 | }; | |
215 | ||
490565d0 | 216 | this.addEntity = function(type, selectTab) { |
23fd8685 | 217 | var meta = afGui.meta.entities[type], |
b844d2ca CW |
218 | num = 1; |
219 | // Give this new entity a unique name | |
220 | while (!!$scope.entities[type + num]) { | |
221 | num++; | |
222 | } | |
5543e896 | 223 | $scope.entities[type + num] = backfillEntityDefaults(_.assign($parse(meta.defaults)(editor), { |
b844d2ca CW |
224 | '#tag': 'af-entity', |
225 | type: meta.entity, | |
226 | name: type + num, | |
490565d0 CW |
227 | label: meta.label + ' ' + num, |
228 | loading: true, | |
3a2ca4f5 | 229 | })); |
490565d0 CW |
230 | |
231 | function addToCanvas() { | |
232 | // Add this af-entity tag after the last existing one | |
233 | var pos = 1 + _.findLastIndex(editor.layout['#children'], {'#tag': 'af-entity'}); | |
234 | editor.layout['#children'].splice(pos, 0, $scope.entities[type + num]); | |
235 | // Create a new af-fieldset container for the entity | |
e1f79950 CW |
236 | if (meta.boilerplate !== false) { |
237 | var fieldset = _.cloneDeep(afGui.meta.elements.fieldset.element); | |
238 | fieldset['af-fieldset'] = type + num; | |
239 | fieldset['af-title'] = meta.label + ' ' + num; | |
240 | // Add boilerplate contents | |
241 | _.each(meta.boilerplate, function (tag) { | |
242 | fieldset['#children'].push(tag); | |
243 | }); | |
244 | // Attempt to place the new af-fieldset after the last one on the form | |
245 | pos = 1 + _.findLastIndex(editor.layout['#children'], 'af-fieldset'); | |
246 | if (pos) { | |
247 | editor.layout['#children'].splice(pos, 0, fieldset); | |
248 | } else { | |
249 | editor.layout['#children'].push(fieldset); | |
250 | } | |
490565d0 CW |
251 | } |
252 | delete $scope.entities[type + num].loading; | |
253 | if (selectTab) { | |
254 | editor.selectEntity(type + num); | |
933b8520 CW |
255 | $timeout(function() { |
256 | editor.scrollToEntity(type + num); | |
257 | }); | |
490565d0 | 258 | } |
4da60774 | 259 | $timeout(editor.adjustTabWidths); |
490565d0 CW |
260 | } |
261 | ||
262 | if (meta.fields) { | |
263 | addToCanvas(); | |
b844d2ca | 264 | } else { |
d617083a | 265 | $timeout(editor.adjustTabWidths); |
490565d0 CW |
266 | crmApi4('Afform', 'loadAdminData', { |
267 | definition: {type: 'form'}, | |
268 | entity: type | |
269 | }, 0).then(function(data) { | |
270 | afGui.addMeta(data); | |
271 | addToCanvas(); | |
272 | }); | |
b844d2ca | 273 | } |
b844d2ca CW |
274 | }; |
275 | ||
276 | this.removeEntity = function(entityName) { | |
277 | delete $scope.entities[entityName]; | |
23fd8685 CW |
278 | afGui.removeRecursive(editor.layout['#children'], {'#tag': 'af-entity', name: entityName}); |
279 | afGui.removeRecursive(editor.layout['#children'], {'af-fieldset': entityName}); | |
b844d2ca CW |
280 | this.selectEntity(null); |
281 | }; | |
282 | ||
283 | this.selectEntity = function(entityName) { | |
284 | $scope.selectedEntityName = entityName; | |
4da60774 | 285 | $timeout(editor.adjustTabWidths); |
b844d2ca CW |
286 | }; |
287 | ||
288 | this.getEntity = function(entityName) { | |
289 | return $scope.entities[entityName]; | |
290 | }; | |
291 | ||
292 | this.getSelectedEntityName = function() { | |
293 | return $scope.selectedEntityName; | |
294 | }; | |
295 | ||
d617083a | 296 | this.getEntityDefn = function(entity) { |
1c2bf8ee | 297 | if (entity.type === 'Contact' && entity.data && entity.data.contact_type) { |
d617083a CW |
298 | return editor.meta.entities[entity.data.contact_type]; |
299 | } | |
300 | return editor.meta.entities[entity.type]; | |
301 | }; | |
302 | ||
933b8520 CW |
303 | // Scroll an entity's first fieldset into view of the canvas |
304 | this.scrollToEntity = function(entityName) { | |
305 | var $canvas = $('#afGuiEditor-canvas-body'), | |
306 | $entity = $('.af-gui-container-type-fieldset[data-entity="' + entityName + '"]').first(), | |
1c2bf8ee CW |
307 | scrollValue, maxScroll; |
308 | if ($entity.length) { | |
933b8520 | 309 | // Scrolltop value needed to place entity's fieldset at top of canvas |
1c2bf8ee | 310 | scrollValue = $canvas.scrollTop() + ($entity.offset().top - $canvas.offset().top); |
933b8520 CW |
311 | // Maximum possible scrollTop (height minus contents height, adjusting for padding) |
312 | maxScroll = $('#afGuiEditor-canvas-body > *').height() - $canvas.height() + 20; | |
1c2bf8ee CW |
313 | // Exceeding the maximum scrollTop breaks the animation so keep it under the limit |
314 | $canvas.animate({scrollTop: scrollValue > maxScroll ? maxScroll : scrollValue}, 500); | |
315 | } | |
933b8520 CW |
316 | }; |
317 | ||
2652d11b | 318 | this.getAfform = function() { |
d617083a | 319 | return editor.afform; |
2652d11b CW |
320 | }; |
321 | ||
783a2874 CW |
322 | this.getEntities = function(filter) { |
323 | return filter ? _.filter($scope.entities, filter) : _.toArray($scope.entities); | |
324 | }; | |
325 | ||
c63f20d3 | 326 | this.toggleContactSummary = function() { |
d617083a CW |
327 | if (editor.afform.contact_summary) { |
328 | editor.afform.contact_summary = false; | |
8d5faaa3 CW |
329 | _.each(editor.searchDisplays, function(searchDisplay) { |
330 | delete searchDisplay.element.filters; | |
331 | }); | |
c63f20d3 | 332 | } else { |
d617083a | 333 | editor.afform.contact_summary = 'block'; |
8d5faaa3 CW |
334 | _.each(editor.searchDisplays, function(searchDisplay) { |
335 | var filterOptions = getSearchFilterOptions(searchDisplay.settings); | |
336 | if (filterOptions.length) { | |
337 | searchDisplay.element.filters = filterOptions[0].key; | |
338 | } | |
339 | }); | |
c63f20d3 CW |
340 | } |
341 | }; | |
342 | ||
7b9f8bdc CW |
343 | this.toggleNavigation = function() { |
344 | if (editor.afform.navigation) { | |
345 | editor.afform.navigation = null; | |
346 | } else { | |
347 | loadNavigationMenu(); | |
348 | editor.afform.navigation = { | |
349 | parent: null, | |
350 | label: editor.afform.title, | |
351 | weight: 0 | |
352 | }; | |
353 | } | |
354 | }; | |
355 | ||
356 | function loadNavigationMenu() { | |
357 | if ('navigationMenu' in editor) { | |
358 | return; | |
359 | } | |
360 | editor.navigationMenu = null; | |
361 | var conditions = [ | |
362 | ['domain_id', '=', 'current_domain'], | |
363 | ['name', '!=', 'Home'] | |
364 | ]; | |
365 | if (editor.afform.name) { | |
366 | conditions.push(['name', '!=', editor.afform.name]); | |
367 | } | |
368 | crmApi4('Navigation', 'get', { | |
369 | select: ['name', 'label', 'parent_id', 'icon'], | |
370 | where: conditions, | |
371 | orderBy: {weight: 'ASC'} | |
372 | }).then(function(items) { | |
373 | editor.navigationMenu = buildTree(items, null); | |
374 | }); | |
375 | } | |
376 | ||
377 | function buildTree(items, parentId) { | |
378 | return _.transform(items, function(navigationMenu, item) { | |
379 | if (parentId === item.parent_id) { | |
380 | var children = buildTree(items, item.id), | |
381 | menuItem = { | |
382 | id: item.name, | |
383 | text: item.label, | |
384 | icon: item.icon | |
385 | }; | |
386 | if (children.length) { | |
387 | menuItem.children = children; | |
388 | } | |
389 | navigationMenu.push(menuItem); | |
390 | } | |
391 | }, []); | |
392 | } | |
393 | ||
7cc347c2 CW |
394 | // Collects all search displays currently on the form |
395 | function getSearchDisplaysOnForm() { | |
396 | var searchFieldsets = afGui.findRecursive(editor.afform.layout, {'af-fieldset': ''}); | |
397 | return _.transform(searchFieldsets, function(searchDisplays, fieldset) { | |
398 | var displayElement = afGui.findRecursive(fieldset['#children'], function(item) { | |
399 | return item['search-name'] && item['#tag'] && item['#tag'].indexOf('crm-search-display-') === 0; | |
400 | })[0]; | |
401 | if (displayElement) { | |
402 | searchDisplays[displayElement['search-name'] + (displayElement['display-name'] ? '.' + displayElement['display-name'] : '')] = { | |
403 | element: displayElement, | |
404 | fieldset: fieldset, | |
405 | settings: afGui.getSearchDisplay(displayElement['search-name'], displayElement['display-name']) | |
406 | }; | |
407 | } | |
408 | }, {}); | |
409 | } | |
410 | ||
411 | // Load data for "Add search display" dropdown | |
412 | this.getSearchDisplaySelector = function() { | |
413 | // Reset search input in dropdown | |
414 | $scope.searchDisplayListFilter.label = ''; | |
415 | // A value means it's alredy loaded. Null means it's loading. | |
416 | if (!editor.searchOptions && editor.searchOptions !== null) { | |
417 | editor.searchOptions = null; | |
418 | afGui.getAllSearchDisplays().then(function(links) { | |
419 | editor.searchOptions = links; | |
420 | }); | |
421 | } | |
422 | }; | |
423 | ||
424 | this.addSearchDisplay = function(display) { | |
425 | var searchName = display.key.split('.')[0]; | |
426 | var displayName = display.key.split('.')[1] || ''; | |
427 | var fieldset = { | |
428 | '#tag': 'div', | |
429 | 'af-fieldset': '', | |
be125358 | 430 | 'af-title': display.label, |
7cc347c2 CW |
431 | '#children': [ |
432 | { | |
433 | '#tag': display.tag, | |
434 | 'search-name': searchName, | |
435 | 'display-name': displayName, | |
436 | } | |
437 | ] | |
438 | }; | |
439 | var meta = { | |
440 | fieldset: fieldset, | |
441 | element: fieldset['#children'][0], | |
442 | settings: afGui.getSearchDisplay(searchName, displayName), | |
443 | }; | |
444 | editor.searchDisplays[display.key] = meta; | |
445 | ||
446 | function addToCanvas() { | |
447 | editor.layout['#children'].push(fieldset); | |
448 | editor.selectEntity(display.key); | |
449 | } | |
450 | if (meta.settings) { | |
451 | addToCanvas(); | |
452 | } else { | |
453 | $timeout(editor.adjustTabWidths); | |
454 | crmApi4('Afform', 'loadAdminData', { | |
455 | definition: {type: 'search'}, | |
456 | entity: display.key | |
457 | }, 0).then(function(data) { | |
458 | afGui.addMeta(data); | |
459 | meta.settings = afGui.getSearchDisplay(searchName, displayName); | |
460 | addToCanvas(); | |
461 | }); | |
462 | } | |
463 | }; | |
464 | ||
465 | // Triggered by afGuiContainer.removeElement | |
466 | this.onRemoveElement = function() { | |
467 | // Keep this.searchDisplays in-sync when deleteing stuff from the form | |
468 | if (editor.getFormType() === 'search') { | |
469 | var current = getSearchDisplaysOnForm(); | |
470 | _.each(_.keys(editor.searchDisplays), function(key) { | |
471 | if (!(key in current)) { | |
472 | delete editor.searchDisplays[key]; | |
473 | editor.selectEntity(null); | |
474 | } | |
475 | }); | |
476 | } | |
477 | }; | |
478 | ||
479 | // This function used to be needed to build a menu of available contact_id fields | |
480 | // but is no longer used for that and is overkill for what it does now. | |
8d5faaa3 CW |
481 | function getSearchFilterOptions(searchDisplay) { |
482 | var | |
c63f20d3 CW |
483 | entityCount = {}, |
484 | options = []; | |
485 | ||
5c952e51 | 486 | addFields(searchDisplay['saved_search_id.api_entity'], ''); |
c63f20d3 | 487 | |
5c952e51 | 488 | _.each(searchDisplay['saved_search_id.api_params'].join, function(join) { |
c63f20d3 CW |
489 | var joinInfo = join[0].split(' AS '); |
490 | addFields(joinInfo[0], joinInfo[1] + '.'); | |
491 | }); | |
492 | ||
493 | function addFields(entityName, prefix) { | |
494 | var entity = afGui.getEntity(entityName); | |
495 | entityCount[entity.entity] = (entityCount[entity.entity] || 0) + 1; | |
496 | var count = (entityCount[entity.entity] > 1 ? ' ' + entityCount[entity.entity] : ''); | |
497 | if (entityName === 'Contact') { | |
498 | options.push({ | |
499 | key: "{'" + prefix + "id': options.contact_id}", | |
500 | label: entity.label + count | |
501 | }); | |
502 | } else { | |
503 | _.each(entity.fields, function(field) { | |
504 | if (field.fk_entity === 'Contact') { | |
505 | options.push({ | |
506 | key: "{'" + prefix + field.name + "': options.contact_id}", | |
507 | label: entity.label + count + ' ' + field.label | |
508 | }); | |
509 | } | |
510 | }); | |
511 | } | |
512 | } | |
513 | return options; | |
514 | } | |
515 | ||
d617083a CW |
516 | this.getLink = function() { |
517 | if (editor.afform.server_route) { | |
518 | return CRM.url(editor.afform.server_route, null, editor.afform.is_public ? 'front' : 'back'); | |
519 | } | |
520 | }; | |
521 | ||
4da60774 CW |
522 | // Options for ui-sortable in field palette |
523 | this.getSortableOptions = function(entityName) { | |
524 | if (!sortableOptions[entityName + '']) { | |
525 | sortableOptions[entityName + ''] = { | |
4da60774 CW |
526 | appendTo: '#afGuiEditor-canvas-body > af-gui-container', |
527 | containment: '#afGuiEditor-canvas-body', | |
528 | update: editor.onDrop, | |
529 | items: '> div:not(.disabled)', | |
530 | connectWith: '#afGuiEditor-canvas ' + (entityName ? '[data-entity="' + entityName + '"] > ' : '') + '[ui-sortable]', | |
531 | placeholder: 'af-gui-dropzone', | |
ad00103f CW |
532 | scrollSpeed: 8, |
533 | helper: function(e, $el) { | |
534 | // Prevent draggable item from being too large for the drop zones. | |
535 | return $el.clone().css({width: '50px', height: '20px'}); | |
536 | } | |
4da60774 CW |
537 | }; |
538 | } | |
539 | return sortableOptions[entityName + '']; | |
540 | }; | |
541 | ||
b844d2ca CW |
542 | // Validates that a drag-n-drop action is allowed |
543 | this.onDrop = function(event, ui) { | |
544 | var sort = ui.item.sortable; | |
545 | // Check if this is a callback for an item dropped into a different container | |
546 | // @see https://github.com/angular-ui/ui-sortable notes on canceling | |
547 | if (!sort.received && sort.source[0] !== sort.droptarget[0]) { | |
548 | var $source = $(sort.source[0]), | |
549 | $target = $(sort.droptarget[0]), | |
550 | $item = $(ui.item[0]); | |
551 | // Fields cannot be dropped outside their own entity | |
261a6a5d | 552 | if ($item.find('af-gui-field').length) { |
b844d2ca CW |
553 | if ($source.closest('[data-entity]').attr('data-entity') !== $target.closest('[data-entity]').attr('data-entity')) { |
554 | return sort.cancel(); | |
555 | } | |
556 | } | |
557 | // Entity-fieldsets cannot be dropped into other entity-fieldsets | |
261a6a5d | 558 | if ((sort.model['af-fieldset'] || $item.find('.af-gui-fieldset').length) && $target.closest('.af-gui-fieldset').length) { |
b844d2ca CW |
559 | return sort.cancel(); |
560 | } | |
561 | } | |
562 | }; | |
563 | ||
b844d2ca | 564 | $scope.save = function() { |
d617083a | 565 | var afform = JSON.parse(angular.toJson(editor.afform)); |
34f37d59 CW |
566 | // This might be set to undefined by validation |
567 | afform.server_route = afform.server_route || ''; | |
835aeacb | 568 | $scope.saving = true; |
34f37d59 | 569 | crmApi4('Afform', 'save', {formatWhitespace: true, records: [afform]}) |
b844d2ca CW |
570 | .then(function (data) { |
571 | $scope.saving = false; | |
835aeacb CW |
572 | // When saving a new form for the first time |
573 | if (!editor.afform.name) { | |
574 | undoAction = 'save'; | |
575 | editor.afform.name = data[0].name; | |
490565d0 | 576 | } |
835aeacb CW |
577 | // Update undo history - mark current snapshot as "saved" |
578 | _.each(undoHistory, function(snapshot, index) { | |
579 | snapshot.saved = index === undoPosition; | |
580 | snapshot.afform.name = data[0].name; | |
581 | }); | |
1d678002 CW |
582 | if (!angular.equals(afform.navigation, lastSaved.navigation) || |
583 | (afform.server_route !== lastSaved.server_route && afform.navigation) | |
584 | (afform.icon !== lastSaved.icon && afform.navigation) | |
585 | ) { | |
586 | refreshMenubar(); | |
587 | } | |
588 | setLastSaved(); | |
b844d2ca CW |
589 | }); |
590 | }; | |
591 | ||
d617083a | 592 | $scope.$watch('editor.afform.title', function(newTitle, oldTitle) { |
b844d2ca CW |
593 | if (typeof oldTitle === 'string') { |
594 | _.each($scope.entities, function(entity) { | |
5543e896 | 595 | if (entity.data && 'source' in entity.data && (entity.data.source || '') === oldTitle) { |
b844d2ca CW |
596 | entity.data.source = newTitle; |
597 | } | |
598 | }); | |
599 | } | |
600 | }); | |
4da60774 | 601 | |
1d678002 CW |
602 | // Sets last-saved form metadata (used to determine if the menubar needs refresh) |
603 | function setLastSaved() { | |
604 | lastSaved = JSON.parse(angular.toJson(editor.afform)); | |
605 | delete lastSaved.layout; | |
606 | } | |
607 | ||
608 | // Force-refresh the menubar to instantly display the afform menu item | |
609 | function refreshMenubar() { | |
610 | CRM.menubar.destroy(); | |
611 | CRM.menubar.cacheCode = Math.random(); | |
612 | CRM.menubar.initialize(); | |
613 | } | |
614 | ||
4da60774 CW |
615 | // Force editor panels to a fixed height, to avoid palette scrolling offscreen |
616 | function fixEditorHeight() { | |
617 | var height = $(window).height() - $('#afGuiEditor').offset().top; | |
618 | $('#afGuiEditor').height(Math.floor(height)); | |
619 | } | |
620 | ||
621 | // Compress tabs on small screens | |
622 | this.adjustTabWidths = function() { | |
623 | $('#afGuiEditor .panel-heading ul.nav-tabs li.active').css('max-width', ''); | |
624 | $('#afGuiEditor .panel-heading ul.nav-tabs').each(function() { | |
625 | var remainingSpace = Math.floor($(this).width()) - 1, | |
626 | inactiveTabs = $(this).children('li.fluid-width-tab').not('.active'); | |
627 | $(this).children('.active,:not(.fluid-width-tab)').each(function() { | |
628 | remainingSpace -= $(this).width(); | |
629 | }); | |
630 | if (inactiveTabs.length) { | |
631 | inactiveTabs.css('max-width', Math.floor(remainingSpace / inactiveTabs.length) + 'px'); | |
632 | } | |
633 | }); | |
634 | }; | |
b844d2ca CW |
635 | } |
636 | }); | |
637 | ||
638 | })(angular, CRM.$, CRM._); |