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');
23 $scope
.crmUrl
= CRM
.url
;
26 $scope
.saving
= false;
27 $scope
.selectedEntityName
= null;
28 this.meta
= afGui
.meta
;
32 this.$onInit = function() {
33 // Load the current form plus blocks & fields
35 afGui
.addMeta(this.data
);
38 $timeout(fixEditorHeight
);
39 $timeout(editor
.adjustTabWidths
);
41 .on('resize.afGuiEditor', fixEditorHeight
)
42 .on('resize.afGuiEditor', editor
.adjustTabWidths
);
45 this.$onDestroy = function() {
46 $(window
).off('.afGuiEditor');
49 // Initialize the current form
50 function initializeForm() {
51 $scope
.afform
= editor
.data
.definition
;
53 alert('Error: unknown form');
55 if (editor
.mode
=== 'clone') {
56 delete $scope
.afform
.name
;
57 delete $scope
.afform
.server_route
;
58 $scope
.afform
.is_dashlet
= false;
59 $scope
.afform
.title
+= ' ' + ts('(copy)');
61 $scope
.canvasTab
= 'layout';
62 $scope
.layoutHtml
= '';
63 editor
.layout
= {'#children': []};
66 if (editor
.getFormType() === 'form') {
67 editor
.allowEntityConfig
= true;
68 editor
.layout
['#children'] = afGui
.findRecursive($scope
.afform
.layout
, {'#tag': 'af-form'})[0]['#children'];
69 $scope
.entities
= _
.mapValues(afGui
.findRecursive(editor
.layout
['#children'], {'#tag': 'af-entity'}, 'name'), backfillEntityDefaults
);
71 if (editor
.mode
=== 'create') {
72 editor
.addEntity(editor
.entity
);
73 editor
.layout
['#children'].push(afGui
.meta
.elements
.submit
.element
);
77 else if (editor
.getFormType() === 'block') {
78 editor
.layout
['#children'] = $scope
.afform
.layout
;
79 editor
.blockEntity
= $scope
.afform
.join
|| $scope
.afform
.block
;
80 $scope
.entities
[editor
.blockEntity
] = backfillEntityDefaults({
81 type
: editor
.blockEntity
,
82 name
: editor
.blockEntity
,
83 label
: afGui
.getEntity(editor
.blockEntity
).label
87 else if (editor
.getFormType() === 'search') {
88 editor
.layout
['#children'] = afGui
.findRecursive($scope
.afform
.layout
, {'af-fieldset': ''})[0]['#children'];
89 editor
.searchDisplay
= afGui
.findRecursive(editor
.layout
['#children'], function(item
) {
90 return item
['#tag'] && item
['#tag'].indexOf('crm-search-display-') === 0;
92 editor
.searchFilters
= getSearchFilterOptions();
95 // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
96 $scope
.changesSaved
= editor
.mode
=== 'edit' ? 1 : false;
97 $scope
.$watch('afform', function () {
98 $scope
.changesSaved
= $scope
.changesSaved
=== 1;
102 this.getFormType = function() {
103 return $scope
.afform
.type
;
106 $scope
.updateLayoutHtml = function() {
107 $scope
.layoutHtml
= '...Loading...';
108 crmApi4('Afform', 'convert', {layout
: $scope
.afform
.layout
, from: 'deep', to
: 'html', formatWhitespace
: true})
110 $scope
.layoutHtml
= r
[0].layout
|| '(Error)';
113 $scope
.layoutHtml
= '(Error)';
117 this.addEntity = function(type
, selectTab
) {
118 var meta
= afGui
.meta
.entities
[type
],
120 // Give this new entity a unique name
121 while (!!$scope
.entities
[type
+ num
]) {
124 $scope
.entities
[type
+ num
] = backfillEntityDefaults(_
.assign($parse(meta
.defaults
)($scope
), {
128 label
: meta
.label
+ ' ' + num
,
132 function addToCanvas() {
133 // Add this af-entity tag after the last existing one
134 var pos
= 1 + _
.findLastIndex(editor
.layout
['#children'], {'#tag': 'af-entity'});
135 editor
.layout
['#children'].splice(pos
, 0, $scope
.entities
[type
+ num
]);
136 // Create a new af-fieldset container for the entity
137 var fieldset
= _
.cloneDeep(afGui
.meta
.elements
.fieldset
.element
);
138 fieldset
['af-fieldset'] = type
+ num
;
139 fieldset
['#children'][0]['#children'][0]['#text'] = meta
.label
+ ' ' + num
;
140 // Add boilerplate contents
141 _
.each(meta
.boilerplate
, function (tag
) {
142 fieldset
['#children'].push(tag
);
144 // Attempt to place the new af-fieldset after the last one on the form
145 pos
= 1 + _
.findLastIndex(editor
.layout
['#children'], 'af-fieldset');
147 editor
.layout
['#children'].splice(pos
, 0, fieldset
);
149 editor
.layout
['#children'].push(fieldset
);
151 delete $scope
.entities
[type
+ num
].loading
;
153 editor
.selectEntity(type
+ num
);
155 $timeout(editor
.adjustTabWidths
);
161 crmApi4('Afform', 'loadAdminData', {
162 definition
: {type
: 'form'},
164 }, 0).then(function(data
) {
171 this.removeEntity = function(entityName
) {
172 delete $scope
.entities
[entityName
];
173 afGui
.removeRecursive(editor
.layout
['#children'], {'#tag': 'af-entity', name
: entityName
});
174 afGui
.removeRecursive(editor
.layout
['#children'], {'af-fieldset': entityName
});
175 this.selectEntity(null);
178 this.selectEntity = function(entityName
) {
179 $scope
.selectedEntityName
= entityName
;
180 $timeout(editor
.adjustTabWidths
);
183 this.getEntity = function(entityName
) {
184 return $scope
.entities
[entityName
];
187 this.getSelectedEntityName = function() {
188 return $scope
.selectedEntityName
;
191 this.getAfform = function() {
192 return $scope
.afform
;
195 this.toggleContactSummary = function() {
196 if ($scope
.afform
.contact_summary
) {
197 $scope
.afform
.contact_summary
= false;
198 if ($scope
.afform
.type
=== 'search') {
199 delete editor
.searchDisplay
.filters
;
202 $scope
.afform
.contact_summary
= 'block';
203 if ($scope
.afform
.type
=== 'search') {
204 editor
.searchDisplay
.filters
= editor
.searchFilters
[0].key
;
209 function getSearchFilterOptions() {
210 var searchDisplay
= editor
.meta
.searchDisplays
[editor
.searchDisplay
['search-name'] + '.' + editor
.searchDisplay
['display-name']],
214 addFields(searchDisplay
['saved_search.api_entity'], '');
216 _
.each(searchDisplay
['saved_search.api_params'].join
, function(join
) {
217 var joinInfo
= join
[0].split(' AS ');
218 addFields(joinInfo
[0], joinInfo
[1] + '.');
221 function addFields(entityName
, prefix
) {
222 var entity
= afGui
.getEntity(entityName
);
223 entityCount
[entity
.entity
] = (entityCount
[entity
.entity
] || 0) + 1;
224 var count
= (entityCount
[entity
.entity
] > 1 ? ' ' + entityCount
[entity
.entity
] : '');
225 if (entityName
=== 'Contact') {
227 key
: "{'" + prefix
+ "id': options.contact_id}",
228 label
: entity
.label
+ count
231 _
.each(entity
.fields
, function(field
) {
232 if (field
.fk_entity
=== 'Contact') {
234 key
: "{'" + prefix
+ field
.name
+ "': options.contact_id}",
235 label
: entity
.label
+ count
+ ' ' + field
.label
244 // Options for ui-sortable in field palette
245 this.getSortableOptions = function(entityName
) {
246 if (!sortableOptions
[entityName
+ '']) {
247 sortableOptions
[entityName
+ ''] = {
249 appendTo
: '#afGuiEditor-canvas-body > af-gui-container',
250 containment
: '#afGuiEditor-canvas-body',
251 update
: editor
.onDrop
,
252 items
: '> div:not(.disabled)',
253 connectWith
: '#afGuiEditor-canvas ' + (entityName
? '[data-entity="' + entityName
+ '"] > ' : '') + '[ui-sortable]',
254 placeholder
: 'af-gui-dropzone',
255 tolerance
: 'pointer',
259 return sortableOptions
[entityName
+ ''];
262 // Validates that a drag-n-drop action is allowed
263 this.onDrop = function(event
, ui
) {
264 var sort
= ui
.item
.sortable
;
265 // Check if this is a callback for an item dropped into a different container
266 // @see https://github.com/angular-ui/ui-sortable notes on canceling
267 if (!sort
.received
&& sort
.source
[0] !== sort
.droptarget
[0]) {
268 var $source
= $(sort
.source
[0]),
269 $target
= $(sort
.droptarget
[0]),
270 $item
= $(ui
.item
[0]);
271 // Fields cannot be dropped outside their own entity
272 if ($item
.find('af-gui-field').length
) {
273 if ($source
.closest('[data-entity]').attr('data-entity') !== $target
.closest('[data-entity]').attr('data-entity')) {
274 return sort
.cancel();
277 // Entity-fieldsets cannot be dropped into other entity-fieldsets
278 if ((sort
.model
['af-fieldset'] || $item
.find('.af-gui-fieldset').length
) && $target
.closest('.af-gui-fieldset').length
) {
279 return sort
.cancel();
284 $scope
.save = function() {
285 var afform
= JSON
.parse(angular
.toJson($scope
.afform
));
286 // This might be set to undefined by validation
287 afform
.server_route
= afform
.server_route
|| '';
288 $scope
.saving
= $scope
.changesSaved
= true;
289 crmApi4('Afform', 'save', {formatWhitespace
: true, records
: [afform
]})
290 .then(function (data
) {
291 $scope
.saving
= false;
292 $scope
.afform
.name
= data
[0].name
;
293 if (editor
.mode
!== 'edit') {
294 $location
.url('/edit/' + data
[0].name
);
299 $scope
.$watch('afform.title', function(newTitle
, oldTitle
) {
300 if (typeof oldTitle
=== 'string') {
301 _
.each($scope
.entities
, function(entity
) {
302 if (entity
.data
&& entity
.data
.source
=== oldTitle
) {
303 entity
.data
.source
= newTitle
;
309 // Force editor panels to a fixed height, to avoid palette scrolling offscreen
310 function fixEditorHeight() {
311 var height
= $(window
).height() - $('#afGuiEditor').offset().top
;
312 $('#afGuiEditor').height(Math
.floor(height
));
315 // Compress tabs on small screens
316 this.adjustTabWidths = function() {
317 $('#afGuiEditor .panel-heading ul.nav-tabs li.active').css('max-width', '');
318 $('#afGuiEditor .panel-heading ul.nav-tabs').each(function() {
319 var remainingSpace
= Math
.floor($(this).width()) - 1,
320 inactiveTabs
= $(this).children('li.fluid-width-tab').not('.active');
321 $(this).children('.active,:not(.fluid-width-tab)').each(function() {
322 remainingSpace
-= $(this).width();
324 if (inactiveTabs
.length
) {
325 inactiveTabs
.css('max-width', Math
.floor(remainingSpace
/ inactiveTabs
.length
) + 'px');
332 })(angular
, CRM
.$, CRM
._
);