(function(angular, $, _) { angular.module('ng') .config(['$locationProvider', function($locationProvider) { $locationProvider.hashPrefix(''); }]); angular.module('af', CRM.angRequires('af')); angular.module('af').directive('afButton', function() { return { restrict: 'C', bindToController: { type: '@' }, require: { afForm: '?^^afForm', }, controller: function($scope, $element) { const ctrl = this; this.$onInit = function () { const type = $element.attr('type'); if (type === 'reset') { $element.on('click', function(e) { e.preventDefault(); $scope.$apply(function() { if (ctrl.afForm) { ctrl.afForm.resetForm(); } else { $scope.$parent.$broadcast('afFormReset'); } }); }); } }; } }; }); angular.module('af').component('afCharacterCount', { bindings: { maxlength: '<', getter: '<', }, templateUrl: '~/af/afCharacterCount.html', controller: function($scope, $element) { this.$onInit = function() { }; this.getLength = function() { return (this.getter() || '').length; }; this.getStatusClass = function() { let fraction = this.getLength() / (this.maxlength || 1); if (fraction > 0.9) { return 'text-danger'; } if (fraction > 0.7) { return 'text-warning'; } return 'text-success'; }; } }); const modelProps = { type: '@', data: '=', actions: '=', modelName: '@name', label: '@' }; angular.module('af').component('afEntity', { require: {afForm: '^afForm'}, bindings: modelProps, controller: function() { this.$onInit = function() { const entity = Object.keys(modelProps).reduce((obj, key) => { if (this[key] !== undefined) { obj[key] = this[key]; } return obj; }, {}); entity.actions = entity.actions || {update: true, create: true}; entity.id = null; this.afForm.registerEntity(entity); }; } }); let afFieldId = 0; angular.module('af').component('afField', { require: { afFieldset: '^^afFieldset', afJoin: '?^^afJoin', afRepeatItem: '?^^afRepeatItem' }, templateUrl: '~/af/afField.html', bindings: { fieldName: '@name', defn: '=' }, controller: function($scope, $element, crmApi4, $timeout) { const ts = $scope.ts = CRM.ts('org.civicrm.afform'); const ctrl = this; let namePrefix = ''; let fieldOptions = null; this.inputAttrs = []; this.$onInit = function() { const closestController = $($element).closest('[af-fieldset],[af-join],[af-repeat-item]'); $scope.dataProvider = closestController.is('[af-repeat-item]') ? ctrl.afRepeatItem : ctrl.afJoin || ctrl.afFieldset; $scope.fieldId = _.kebabCase(ctrl.fieldName) + '-' + afFieldId++; $element.addClass('af-field-type-' + _.kebabCase(ctrl.defn.input_type)); if (this.defn.input_attrs && this.defn.input_attrs.multiple) { $element.addClass('af-field-type-multiple'); } this.fkEntity = this.defn.fk_entity || null; if (this.defn.name !== this.fieldName) { if (!this.defn.name) { console.error('Missing field definition for: ' + this.fieldName); return; } namePrefix = this.fieldName.substr(0, this.fieldName.length - this.defn.name.length); } if (this.defn.search_operator) { this.search_operator = this.defn.search_operator; } fieldOptions = this.defn.options || null; if (this.defn.data_type === 'Boolean') { if (fieldOptions) { fieldOptions.forEach((option) => option.id = !!option.id); } else { fieldOptions = [{id: true, label: ts('Yes')}, {id: false, label: ts('No')}]; } } if (ctrl.fieldName === 'is_primary' && 'repeatIndex' in $scope.dataProvider) { fieldOptions = [{id: true, label: ''}]; $scope.$watch('dataProvider.afRepeat.getEntityController().getData()', function (items, prev) { const index = $scope.dataProvider.repeatIndex; if (items && !index && !items.some(item => item.is_primary)) { $scope.dataProvider.getFieldData().is_primary = true; } if (items && prev && items.length === prev.length && items[index].is_primary && prev[index].is_primary && items.filter(item => item.is_primary).length > 1 ) { $scope.dataProvider.getFieldData().is_primary = false; } }, true); } if (ctrl.defn.input_type === 'ChainSelect' && ctrl.defn.input_attrs.control_field) { const controlField = namePrefix + ctrl.defn.input_attrs.control_field; $scope.$watch('dataProvider.getFieldData()["' + controlField + '"]', function(val) { function validateValue() { const options = $scope.getOptions(); let value = $scope.dataProvider.getFieldData()[ctrl.fieldName]; if (Array.isArray(value)) { value.splice(0, value.length, ...value.filter(item => options.some(option => option.id == item) )); } else { if (value && !options.some(option => option.id == value)) { value = ''; } $('input[crm-ui-select]', $element).val(value).change(); } } if (val && (typeof val === 'number' || val.length)) { $('input[crm-ui-select]', $element).addClass('loading').prop('disabled', true); const params = { name: ctrl.afFieldset.getFormName(), modelName: ctrl.afFieldset.getName(), fieldName: ctrl.fieldName, joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null, values: $scope.dataProvider.getFieldData() }; crmApi4('Afform', 'getOptions', params) .then(function(data) { $('input[crm-ui-select]', $element).removeClass('loading').prop('disabled', !data.length); fieldOptions = data; validateValue(); }); } else { fieldOptions = null; validateValue(); } }, true); } if (ctrl.defn.input_type === 'EntityRef' && ctrl.defn.dfk_entities && ctrl.defn.input_attrs.control_field) { const controlField = namePrefix + ctrl.defn.input_attrs.control_field; $scope.$watch('dataProvider.getFieldData()["' + controlField + '"]', function(val) { if (val && val.length) { if (Array.isArray(val)) { ctrl.fkEntity = ctrl.defn.dfk_entities[val[0]]; } else { ctrl.fkEntity = ctrl.defn.dfk_entities[val]; } } else { ctrl.fkEntity = null; } }); } $timeout(function() { initializeValue(true); }); function initializeValue(firstLoad) { const entityName = ctrl.afFieldset.getName(), joinEntity = ctrl.afJoin ? ctrl.afJoin.entity : null, urlArgs = $scope.$parent.routeParams; let uniquePrefix = ''; if (entityName) { const index = ctrl.getEntityIndex(); uniquePrefix = entityName + (index ? index + 1 : '') + (joinEntity ? '.' + joinEntity : '') + '.'; } if (urlArgs && ((uniquePrefix + ctrl.fieldName) in urlArgs)) { setValue(urlArgs[uniquePrefix + ctrl.fieldName]); } else if (urlArgs && (ctrl.fieldName in urlArgs)) { setValue(urlArgs[ctrl.fieldName]); } else if (urlArgs && urlArgs._s) { setValue(ctrl.afFieldset.getSearchParamSetFieldValue(ctrl.fieldName)); } else if (firstLoad && ctrl.afFieldset.getStoredValue(ctrl.fieldName) !== undefined) { setValue(ctrl.afFieldset.getStoredValue(ctrl.fieldName)); } else if ('afform_default' in ctrl.defn) { setValue(ctrl.defn.afform_default); } if (ctrl.defn.search_range) { const initialVal = $scope.dataProvider.getFieldData()[ctrl.fieldName]; if (!Array.isArray($scope.dataProvider.getFieldData()[ctrl.fieldName]) && (ctrl.defn.input_type !== 'Select' || !ctrl.defn.is_date || initialVal === '{}') ) { $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; } if (ctrl.defn.is_date) { ctrl.inputAttrs.push(ctrl.defn.input_attrs || {}); for (let i = 1; i <= 2; ++i) { const attrs = _.cloneDeep(ctrl.defn.input_attrs || {}); attrs.placeholder = attrs['placeholder' + i]; attrs.timePlaceholder = attrs['timePlaceholder' + i]; ctrl.inputAttrs.push(attrs); } } } } $scope.$on('afFormReset', function() { delete $scope.dataProvider.getFieldData()[ctrl.fieldName]; initializeValue(false); }); }; $scope.$on('afIfDestroy', function() { if (ctrl.defn.input_type !== 'DisplayOnly') { delete $scope.dataProvider.getFieldData()[ctrl.fieldName]; } }); function correctValueType(value, dataType) { if (value === null) { return value; } if (Array.isArray(value)) { return value.map((val) => correctValueType(val, dataType)); } else if (dataType === 'Integer' || dataType === 'Float') { return Number(value); } else if (dataType === 'Boolean') { return (value == 1); } return value; } this.isMultiple = function() { return ( (['Select', 'EntityRef', 'ChainSelect'].includes(ctrl.defn.input_type) && ctrl.defn.input_attrs.multiple) || ((ctrl.defn.input_type === 'CheckBox' || ctrl.defn.input_type === 'Toggle') && ctrl.defn.data_type !== 'Boolean') || ((ctrl.defn.input_type === 'Hidden' || ctrl.defn.input_type === 'DisplayOnly') && (ctrl.defn.serialize || ctrl.defn.data_type === 'Array')) ); }; function setValue(value) { if (typeof value === 'string' && ctrl.isMultiple()) { value = value.split(','); } if (typeof value === 'object' && value !== null && ctrl.search_operator) { if (ctrl.defn.expose_operator) { ctrl.search_operator = Object.keys(value)[0]; } value = value[ctrl.search_operator] ? value[ctrl.search_operator] : null; } if (ctrl.defn.input_type === 'EntityRef' && ['Contact', 'Individual'].includes(ctrl.fkEntity) && value === 'user_contact_id') { value = CRM.config.cid; } if (ctrl.defn.input_type !== 'DisplayOnly') { value = correctValueType(value, ctrl.defn.data_type); } if (ctrl.defn.input_type === 'Date' && typeof value === 'string' && value.startsWith('now')) { value = getRelativeDate(value, ctrl.defn.input_attrs.time); } if (ctrl.defn.input_type === 'Number' && ctrl.defn.search_range) { if (!_.isPlainObject(value)) { value = { '>=': +(('' + value).split('-')[0] || 0), '<=': +(('' + value).split('-')[1] || 0), }; } } else if (ctrl.defn.input_type === 'Number') { value = Number(value); } else if (ctrl.defn.search_range && !_.isPlainObject(value) && !(ctrl.defn.options && ctrl.defn.options.some(option => option.id === value)) ) { value = { '>=': ('' + value).split('-')[0], '<=': ('' + value).split('-')[1] || '', }; } $scope.getSetValue(value); } ctrl.getEntityIndex = function() { if ('repeatIndex' in $scope.dataProvider && $scope.dataProvider.afRepeat.getRepeatType() === 'join') { return $scope.dataProvider.outerRepeatItem ? $scope.dataProvider.outerRepeatItem.repeatIndex : 0; } else { return ctrl.afRepeatItem ? ctrl.afRepeatItem.repeatIndex : 0; } }; ctrl.isReadonly = function() { if (ctrl.defn.input_attrs && ctrl.defn.input_attrs.autofill && !ctrl.afJoin) { return ctrl.afFieldset.getEntity().actions[ctrl.defn.input_attrs.autofill] === false; } return ctrl.defn.input_type === 'DisplayOnly'; }; ctrl.isDisabled = function() { if (ctrl.isReadonly()) { return true; } return ctrl.defn.input_type === 'EntityRef' && !ctrl.fkEntity; }; ctrl.getDisplayValue = function(value) { if (value === undefined || value === null || value === '' || (Array.isArray(value) && !value.length)) { return ''; } if (fieldOptions) { let keys = Array.isArray(value) ? value : [value]; let options = fieldOptions.filter((option) => keys.includes(option.id)); return options.map((option) => option.label).join(', '); } if (ctrl.defn.data_type === 'Date' || ctrl.defn.data_type === 'Timestamp') { try { return CRM.utils.formatDate(value, null, ctrl.defn.data_type === 'Timestamp'); } catch (e) { return ''; } } if (ctrl.fkEntity) { const ids = Array.isArray(value) ? value : [value]; if (!ctrl._entityLabels) { ctrl._entityLabels = {}; } if (!(ids.join() in ctrl._entityLabels)) { ctrl._entityLabels[ids.join()] = null; const params = ctrl.getAutocompleteParams(); params.ids = ids; crmApi4(ctrl.fkEntity, 'autocomplete', params) .then(function(result) { ctrl._entityLabels[ids.join()] = result.map((item) => item.label).join(', '); }); } return ctrl._entityLabels[ids.join()] || ts('Loading...'); } return value; }; ctrl.onSelectEntity = function() { if (ctrl.defn.input_attrs && ctrl.defn.input_attrs.autofill) { const val = $scope.getSetSelect(); const entity = ctrl.afFieldset.modelName; const entityIndex = ctrl.getEntityIndex(); const joinEntity = ctrl.afJoin ? ctrl.afJoin.entity : null; const joinIndex = ctrl.afJoin && $scope.dataProvider.repeatIndex || 0; ctrl.afFieldset.afFormCtrl.loadData(entity, entityIndex, val, ctrl.defn.name, joinEntity, joinIndex); } }; ctrl.getFileUploadParams = function() { return { modelName: ctrl.afFieldset.getName(), fieldName: ctrl.fieldName, joinEntity: ctrl.afJoin ? ctrl.afJoin.entity : null, entityIndex: ctrl.getEntityIndex(), joinIndex: ctrl.afJoin && $scope.dataProvider.repeatIndex || null }; }; ctrl.getAutocompleteParams = function() { let fieldName = ctrl.afFieldset.getName(); if (ctrl.afJoin) { fieldName += '+' + ctrl.afJoin.entity; } fieldName += ':' + ctrl.fieldName; return { formName: 'afform:' + ctrl.afFieldset.getFormName(), fieldName: fieldName, values: $scope.dataProvider.getFieldData() }; }; $scope.getOptions = function () { return fieldOptions; }; $scope.select2Options = function() { return { results: _.transform($scope.getOptions(), function(result, opt) { result.push({id: opt.id, text: opt.label}); }, []) }; }; this.onChangeOperator = function() { $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; }; $scope.getSetValue = function(val) { const currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName]; if (arguments.length) { if (ctrl.search_operator) { if (typeof currentVal !== 'object') { $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; } return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.search_operator] = val); } return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val); } if (ctrl.search_operator) { return (currentVal || {})[ctrl.search_operator]; } return currentVal; }; $scope.getSetSelect = function(val) { const currentVal = $scope.dataProvider.getFieldData()[ctrl.fieldName]; if (arguments.length) { if (ctrl.defn.is_date) { if (val === '{}') { val = !_.isPlainObject(currentVal) ? {} : currentVal; } } else if (ctrl.defn.search_range) { return ($scope.dataProvider.getFieldData()[ctrl.fieldName]['>='] = val); } else if (ctrl.search_operator) { if (typeof currentVal !== 'object') { $scope.dataProvider.getFieldData()[ctrl.fieldName] = {}; } return ($scope.dataProvider.getFieldData()[ctrl.fieldName][ctrl.search_operator] = val); } if (ctrl.defn.data_type === 'Boolean') { return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = (val === 'true')); } if (ctrl.defn.data_type === 'Integer' || ctrl.defn.data_type === 'Float') { if (typeof val === 'string') { return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val.length ? Number(val) : null); } else if (Array.isArray(val)) { return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val.map(Number)); } } return ($scope.dataProvider.getFieldData()[ctrl.fieldName] = val); } if (ctrl.defn.is_date) { return _.isPlainObject(currentVal) ? '{}' : currentVal; } else if (ctrl.defn.search_range) { return currentVal['>=']; } else if (ctrl.search_operator) { return (currentVal || {})[ctrl.search_operator]; } else if (!ctrl.isMultiple() && (typeof currentVal === 'boolean' || typeof currentVal === 'number')) { return JSON.stringify(currentVal); } return currentVal; }; function getRelativeDate(dateString, includeTime) { const parts = dateString.split(' '); const baseDate = new Date(); let unit = parts[2] || 'day'; let offset = parseInt(parts[1] || '0', 10); switch (unit) { case 'week': offset *= 7; break; case 'year': offset *= 365; } let newDate = new Date(baseDate.getTime() + offset * 24 * 60 * 60 * 1000); let localYear = newDate.getFullYear(); let localMonth = String(newDate.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed let localDay = String(newDate.getDate()).padStart(2, '0'); let defaultDate = `${localYear}-${localMonth}-${localDay}`; // Format YYYY-MM-DD if (includeTime) { defaultDate += ' ' + newDate.toTimeString().slice(0,8); } return defaultDate; } } }); angular.module('af').directive('afFieldset', function() { return { restrict: 'A', require: ['afFieldset', '?^^afForm'], bindToController: { modelName: '@afFieldset', storeValues: '<' }, link: function($scope, $el, $attr, ctrls) { const self = ctrls[0]; self.afFormCtrl = ctrls[1]; }, controller: function($scope, $element, crmApi4) { const ctrl = this; const localData = []; const joinOffsets = {}; const ts = $scope.ts = CRM.ts('org.civicrm.afform'); this.getData = function() { return ctrl.afFormCtrl ? ctrl.afFormCtrl.getData(ctrl.modelName) : localData; }; this.getName = function() { return this.modelName || $element.find('[search-name][display-name]').attr('display-name'); }; this.getEntity = function() { return this.afFormCtrl.getEntity(this.modelName); }; this.getEntityType = function() { return this.afFormCtrl.getEntity(this.modelName).type; }; this.getFieldData = function() { const data = ctrl.getData(); if (!data.length) { data.push({fields: {}}); } return data[0].fields; }; this.getFormName = function() { return ctrl.afFormCtrl ? ctrl.afFormCtrl.getFormMeta().name : $scope.meta.name; }; this.getFieldMeta = () => { const meta = {}; const fieldElements = $element[0].querySelectorAll('af-field'); fieldElements.forEach((field) => { const name = field.getAttribute('name'); const defn = $scope.$eval(field.getAttribute('defn')); meta[name] = defn; }); return meta; }; this.getJoinOffset = function(joinEntity) { joinOffsets[joinEntity] = joinEntity in joinOffsets ? joinOffsets[joinEntity] + 1 : 0; return joinOffsets[joinEntity]; }; function getCacheKey() { return 'afform:' + ctrl.getFormName() + ctrl.getName(); } this.getStoredValue = function(fieldName) { if (!this.storeValues) { return; } if (!this.reloadedStoredValues) { this.reloadedStoredValues = CRM.cache.get(getCacheKey(), {}); } return this.reloadedStoredValues[fieldName]; }; this.$onInit = function() { $scope.$watch(ctrl.getFieldData, (newVal, oldVal) => { $element[0].dispatchEvent(new Event('crmFormChangeFilters')); if (this.storeValues) { if (typeof newVal === 'object' && typeof oldVal === 'object' && Object.keys(newVal).length) { CRM.cache.set(getCacheKey(), newVal); } } }, true); $scope.$watch(this.getSearchParamSetId, () => this.fetchSearchParamSetValues()); }; /** * Get fieldset values to use for afform filters * * Filter out empty values (null, '', [], {}) at top level and one level * below for object values (fields with ranges/operators) * * @returns Object */ this.getFilterValues = () => { const isEmpty = (v) => { if (v === null || v === '') { return true; } if (Array.isArray(v) && v.length === 0) { return true; } if (typeof v === 'object' && Object.keys(v).length === 0) { return true; } }; const data = _.cloneDeep(this.getFieldData()); Object.keys(data).forEach((key) => { if (typeof data[key] === 'object' && data[key] !== null) { Object.keys(data[key]).forEach((subKey) => { if (isEmpty(data[key][subKey])) { delete data[key][subKey]; } }); } if (isEmpty(data[key])) { delete data[key]; } }); return data; }; /** * Get value for a given field based on currently applied SearchParamSet * Used by afField controller on load */ this.getSearchParamSetFieldValue = (fieldName) => { if (this.selectedSearchParamSet && this.selectedSearchParamSet.filters && this.selectedSearchParamSet.filters.hasOwnProperty(fieldName)) { return this.selectedSearchParamSet.filters[fieldName]; } return null; }; /** * Fetch values for a SearchParamSet from the server */ this.fetchSearchParamSetValues = () => { const searchParamSetId = this.getSearchParamSetId(); if (!searchParamSetId) { return; } crmApi4('SearchParamSet', 'get', { where: [ ['id', '=', searchParamSetId], ['afform_name', '=', this.getFormName()] ], select: ['filters', 'columns'], }) .then((result) => { if (result && result[0]) { this.selectedSearchParamSet = result[0]; } else { this.selectedSearchParamSet = {}; } $scope.$broadcast('afFormReset'); }); }; /** * Get search param set ID from URL hash param */ this.getSearchParamSetId = () => ($scope && $scope.routeParams) ? $scope.routeParams._s : null; } }; }); angular.module('af').component('afForm', { bindings: { ctrl: '@' }, require: { ngForm: 'form' }, controller: function($scope, $element, $timeout, crmApi4, crmStatus, $window, $location, $parse, FileUploader) { const ctrl = this, ts = CRM.ts('org.civicrm.afform'), saveDraftButtons = [], schema = {}, data = {extra: {}}; let status, args, submissionResponse, autoSave = () => {}, draftStatus = 'pristine', cancelDraftWatcher, uploadingDraftFiles = false; this.$onInit = function() { $scope.$parent[this.ctrl] = this; $timeout(function() { ctrl.loadData() .then(setupDraftWatcher); ctrl.showSubmitButton = displaySubmitButton(args); }); }; this.registerEntity = function registerEntity(entity) { schema[entity.modelName] = entity; data[entity.modelName] = []; }; this.getEntity = function getEntity(name) { return schema[name]; }; this.getData = function getData(name) { return data[name]; }; this.getSchema = function getSchema(name) { return schema[name]; }; this.getFormMeta = function getFormMeta() { return $scope.$parent.meta; }; this.resetForm = function() { this.ngForm.$setPristine(); $scope.$parent.$broadcast('afFormReset'); this.loadData(); }; this.loadData = function(selectedEntity, selectedIndex, selectedId, selectedField, joinEntity, joinIndex) { let toLoad = true; const params = {name: ctrl.getFormMeta().name, args: {}}; if (selectedEntity) { toLoad = !!selectedId; params.args[selectedEntity] = {}; params.args[selectedEntity][selectedIndex] = {}; if (joinEntity) { params.fillMode = 'join'; params.args[selectedEntity][selectedIndex].joins = {}; params.args[selectedEntity][selectedIndex].joins[joinEntity] = {}; params.args[selectedEntity][selectedIndex].joins[joinEntity][joinIndex] = {}; params.args[selectedEntity][selectedIndex].joins[joinEntity][joinIndex][selectedField] = selectedId; } else { params.fillMode = 'entity'; params.args[selectedEntity][selectedIndex][selectedField] = selectedId; } } else { params.fillMode = 'form'; args = Object.assign({}, $scope.$parent.routeParams || {}, $scope.$parent.options || {}); Object.keys(schema).forEach(entityName => { if (args[entityName] && typeof args[entityName] === 'string') { args[entityName] = args[entityName].split(','); } }); params.args = args; ctrl.showSubmitButton = displaySubmitButton(args); } if (toLoad) { if (params.fillMode === 'form') { $element.block(); } return crmApi4('Afform', 'prefill', params) .then((result) => { result.forEach((item) => { _.each(item.values, (values, index) => { data[item.name][index] = data[item.name][index] || {}; data[item.name][index].joins = data[item.name][index].joins || {}; angular.merge(data[item.name][index], values, {fields: _.cloneDeep(schema[item.name].data || {})}); }); }); $element.unblock(); }, (error) => { disableForm(error.error_message, ts('Sorry'), 'error'); $element.unblock(); }); } else if (joinEntity) { data[selectedEntity][selectedIndex].joins[joinEntity][joinIndex] = {}; } else if (selectedEntity) { Object.keys(data[selectedEntity][selectedIndex].fields).forEach(key => delete data[selectedEntity][selectedIndex].fields[key]); angular.merge(data[selectedEntity][selectedIndex].fields, _.cloneDeep(schema[selectedEntity].data || {})); data[selectedEntity][selectedIndex].joins = {}; } }; function displaySubmitButton(args) { if (args.sid && args.sid.length > 0) { return false; } return true; } const token = new URLSearchParams(window.location.search).get('_aff'); const headers = {'X-Requested-With': 'XMLHttpRequest'}; if (token) { headers['X-Civi-Auth-Afform'] = token; } this.fileUploader = new FileUploader({ url: CRM.url('civicrm/ajax/api4/Afform/submitFile'), headers: headers, onAfterAddingFile: function(item) { setDraftStatus('unsaved'); }, onSuccessItem: onFileUploadSuccess, onCompleteAll: onFileUploadsComplete, onBeforeUploadItem: function(item) { status.resolve(); status = CRM.status({start: ts('Uploading %1', {1: item.file.name})}); } }); function onFileUploadSuccess(item, response, status, headers) { if (response.values && response.values[0] && response.values[0].id) { const dataProvider = item.crmDataProvider; dataProvider.getFieldData()[item.crmFieldName] = response.values[0]; } } function onFileUploadsComplete() { if (uploadingDraftFiles) { uploadingDraftFiles = false; setDraftStatus('saved'); if (draftStatus === 'unsaved') { autoSave(); } status.resolve(); } else { postProcess(); } } function setupDraftWatcher() { const buttons = getDraftButtons(); const autoSaveEnabled = ctrl.getFormMeta().autosave_draft; if ((!autoSaveEnabled && !buttons.length) || !ctrl.showSubmitButton || !CRM.config.cid) { return; } $.each(buttons, function(index, button) { saveDraftButtons[index] = { text: $(button).text(), icon: $(button).attr('crm-icon'), }; }); if (autoSaveEnabled) { autoSave = _.debounce(ctrl.submitDraft, 10000); } cancelDraftWatcher = $scope.$watch(() => data, function (newVal, oldVal) { if (oldVal) { if (draftStatus === 'pristine') { setDraftStatus('saved'); } else { setDraftStatus('unsaved'); autoSave(newVal); } } }, true ); } this.checkConditions = function(conditions, op) { op = op || 'AND'; let ret = op === 'AND', flip = !ret; _.each(conditions, function(clause) { if (Array.isArray(clause[1])) { if (ctrl.checkConditions(clause[1], clause[0]) === flip) { ret = flip; } } else { if (typeof clause[0] === 'string' && clause[0].charAt(0) !== '"') { clause[0] = clause[0].replace(/\[([^'"])/g, "['$1").replace(/([^'"])]/g, "$1']"); } let parser1 = $parse(clause[0]); let parser2 = $parse(clause[2]); let result = compareConditions(parser1(data), clause[1], parser2(data)); if (result === flip) { ret = flip; } } }); return op === 'NOT' ? !ret : ret; }; function compareConditions(val1, op, val2) { const yes = (op !== '!=' && !op.includes('NOT ')); switch (op) { case '=': case '!=': case '==': if (typeof val1 === 'string') { val1 = val1.toLowerCase(); } if (typeof val2 === 'string') { val2 = val2.toLowerCase(); } return angular.equals(val1, val2) === yes; case '>': return val1 > val2; case '<': return val1 < val2; case '>=': return val1 >= val2; case '<=': return val1 <= val2; case 'IS EMPTY': return !val1; case 'IS NOT EMPTY': return !!val1; case 'CONTAINS': case 'NOT CONTAINS': if (Array.isArray(val1)) { return val1.includes(val2) === yes; } else if (typeof val1 === 'string' && typeof val2 === 'string') { return val1.toLowerCase().includes(val2.toLowerCase()) === yes; } return angular.equals(val1, val2) === yes; case 'IN': case 'NOT IN': if (Array.isArray(val2)) { return val2.includes(val1) === yes; } return angular.equals(val1, val2) === yes; case 'LIKE': case 'NOT LIKE': if (typeof val1 === 'string' && typeof val2 === 'string') { return likeCompare(val1, val2) === yes; } return angular.equals(val1, val2) === yes; } } function likeCompare(str, pattern) { const regexPattern = pattern .replace(/([.+?^=!:${}()|\[\]\/\\])/g, "\\$1") .replace(/%/g, '.*') // Convert % to .* .replace(/_/g, '.'); // Convert _ to . const regex = new RegExp(`^${regexPattern}$`, 'i'); return regex.test(str); } function postProcess() { const metaData = ctrl.getFormMeta(), dialog = $element.closest('.ui-dialog-content'); $element.trigger('crmFormSuccess', { afform: metaData, data: data, submissionResponse: submissionResponse, }); if (submissionResponse[0].redirect) { let url = submissionResponse[0].redirect; if (url.indexOf('civicrm/') === 0) { url = CRM.url(url); } else if (url.indexOf('/') === 0) { let port = $location.port(); port = port ? `:${port}` : ''; url = `${$location.protocol()}://${$location.host()}${port}${url}`; } $window.location.href = url; return; } status.resolve(); if (submissionResponse[0].message) { $element.hide(); const $confirmation = $('
'); $confirmation.html(submissionResponse[0].message); $confirmation.insertAfter($element); } else if (dialog.length) { dialog.dialog('close'); } else { $element.unblock(); } } function validateFileFields() { let valid = true; $("af-form[ng-form=" + ctrl.getFormMeta().name + "] input[type='file']").each((index, fld) => { if ($(fld).attr('required') && $(fld).get(0).files.length == 0) { valid = false; } }); return valid; } function disableForm(errorMsg) { $('af-form[ng-form="' + ctrl.getFormMeta().name + '"]') .addClass('disabled') .find('button[ng-click="afform.submit()"]').prop('disabled', true); CRM.alert(errorMsg, ts('Sorry'), 'error'); } const handleError = (error) => { if (error && error.error_code !== '1') { CRM.alert(error.error_message, ts('Please resolve these issues'), 'warning'); } else { const message = error?.error_message ? error.error_message : ts('Unknown error'); CRM.alert(message, ts('There is a problem'), 'error'); } }; this.submit = function () { if (!ctrl.ngForm.$valid || !validateFileFields()) { if (document.getElementById('crm-notification-container')) { CRM.alert(ts('Please fill all required fields.'), ts('Form Error')); } return; } status = CRM.status({error: ts('Not saved')}); $element.block(); if (cancelDraftWatcher) { cancelDraftWatcher(); } crmApi4('Afform', 'submit', { name: ctrl.getFormMeta().name, args: args, values: data, }).then(function(response) { submissionResponse = response; if (ctrl.fileUploader.getNotUploadedItems().length) { _.each(ctrl.fileUploader.getNotUploadedItems(), function(file) { file.formData.push({ params: JSON.stringify(_.extend({ token: response[0].token, name: ctrl.getFormMeta().name }, file.crmApiParams())) }); }); ctrl.fileUploader.uploadAll(); } else { postProcess(); } }) .catch(function(error) { status.reject(); $element.unblock(); handleError(error); $element.trigger('crmFormError', { afform: ctrl.getFormMeta(), data: data, submissionResponse: submissionResponse, error: error }); }); }; this.submitDraft = function() { if (uploadingDraftFiles) { return; } setDraftStatus('saving'); status = CRM.status({start: ts('Saving Draft'), success: ts('Draft saved')}); crmApi4('Afform', 'submitDraft', { name: ctrl.getFormMeta().name, args: args, values: data, }).then(function(response) { status.resolve(); if (ctrl.fileUploader.getNotUploadedItems().length) { uploadingDraftFiles = true; _.each(ctrl.fileUploader.getNotUploadedItems(), function(file) { file.formData.push({ params: JSON.stringify(_.extend({ name: ctrl.getFormMeta().name }, file.crmApiParams())) }); }); ctrl.fileUploader.uploadAll(); } else { setDraftStatus('saved'); } }) .catch(function(error) { setDraftStatus('unsaved'); handleError(error); }); }; function getDraftButtons() { return $element.find('button[ng-click="afform.submitDraft()"]'); } function setDraftStatus(newStatus) { if (draftStatus === newStatus) { return; } if (draftStatus === 'unsaved' && newStatus === 'saved') { return; } if (newStatus === 'unsaved' && !uploadingDraftFiles) { restoreDraftButtons(); } else if (!uploadingDraftFiles) { const newText = newStatus === 'saving' ? ts('Saving Draft') : ts('Draft Saved'); const newIcon = newStatus === 'saving' ? 'fa-spinner fa-spin' : 'fa-check'; disableDraftButtons(newText, newIcon); } draftStatus = newStatus; } function disableDraftButtons(text, icon) { const buttons = getDraftButtons(); $.each(buttons, function(index, button) { $(button).text(text).attr('disabled', true); $(button).prepend(' '); }); } function restoreDraftButtons() { const buttons = getDraftButtons(); $.each(buttons, function(index, button) { const initialState = saveDraftButtons[index] || saveDraftButtons[0]; $(button).text(initialState.text).attr('disabled', false); if (initialState.icon) { $(button).prepend(' '); } }); } } }); angular.module('af').directive('afIf', function($compile, $animate, $parse) { return { multiElement: true, transclude: 'element', priority: 601, terminal: true, restrict: 'A', require: ['^^afForm'], $$tlb: true, link: function($scope, $element, $attr, ctrl, $transclude) { let block, childScope, previousElements; function watcher() { const conditions = $parse($attr.afIf)(); return ctrl[0].checkConditions(conditions); } $scope.$watch(watcher, function(value) { if (value) { if (!childScope) { $transclude(function(clone, newScope) { childScope = newScope; clone[clone.length++] = $compile.$$createComment('end afIf', $attr.afIf); block = { clone: clone }; $animate.enter(clone, $element.parent(), $element); }); } } else { if (previousElements) { previousElements.remove(); previousElements = null; } if (childScope) { childScope.$broadcast('afIfDestroy'); childScope.$destroy(); childScope = null; } if (block) { previousElements = getBlockNodes(block.clone); $animate.leave(previousElements).done(function(response) { if (response !== false) previousElements = null; }); block = null; } } }); } }; }); /** * Return the DOM siblings between the first and last node in the given array. * @param {Array} array like object * @returns {Array} the inputted object or a jqLite collection containing the nodes */ function getBlockNodes(nodes) { var node = nodes[0]; var endNode = nodes[nodes.length - 1]; var blockNodes; for (var i = 1; node !== endNode && (node = node.nextSibling); i++) { if (blockNodes || nodes[i] !== node) { if (!blockNodes) { blockNodes = $(slice.call(nodes, 0, i)); } blockNodes.push(node); } } return blockNodes || nodes; } angular.module('af') .directive('afJoin', function() { return { restrict: 'A', require: ['afJoin', '^^afFieldset', '?^^afRepeatItem'], bindToController: { entity: '@afJoin', }, link: function($scope, $el, $attr, ctrls) { const self = ctrls[0]; self.afFieldset = ctrls[1]; self.repeatItem = ctrls[2]; self.offset = self.afFieldset.getJoinOffset($attr.afJoin); }, controller: function($scope) { const self = this; this.getEntityType = function() { return this.entity; }; this.getData = function() { let data, fieldsetData; if (self.repeatItem) { data = self.repeatItem.item; } else { fieldsetData = self.afFieldset.getData(); if (!fieldsetData.length) { fieldsetData.push({fields: {}, joins: {}}); } data = fieldsetData[0]; } if (!data.joins) { data.joins = {}; } if (!data.joins[self.entity]) { data.joins[self.entity] = []; } return data.joins[self.entity]; }; this.getFieldData = function() { const data = this.getData(); if (!data[this.offset]) { data[this.offset] = {}; } return data[this.offset]; }; } }; }); angular.module('af') .directive('afRepeat', function() { return { restrict: 'A', require: ['?afFieldset', '?afJoin'], transclude: true, scope: { min: '=', max: '=', addLabel: '@afRepeat', addIcon: '@', copyLabel: '@afCopy', copyIcon: '@' }, templateUrl: '~/af/afRepeat.html', link: function($scope, $el, $attr, ctrls) { $scope.afFieldset = ctrls[0]; $scope.afJoin = ctrls[1]; $scope.element = $el; }, controller: function($scope) { this.getItems = $scope.getItems = function() { const data = getEntityController().getData(); while ($scope.min && data.length < $scope.min) { data.push(getRepeatType() === 'join' ? {} : {fields: {}, joins: {}}); } return data; }; function getRepeatType() { return $scope.afJoin ? 'join' : 'fieldset'; } this.getRepeatType = getRepeatType; function getEntityController() { return $scope.afJoin || $scope.afFieldset; } this.getEntityController = getEntityController; $scope.addItem = function() { $scope.getItems().push(getRepeatType() === 'join' ? {} : {fields: {}}); }; $scope.copyItem = function() { const data = $scope.getItems(); const last = data[data.length - 1]; data.push(getRepeatType() === 'join' ? angular.copy(last) : {fields: angular.copy(last.fields)}); }; $scope.removeItem = function(index) { $scope.getItems().splice(index, 1); }; $scope.canAdd = function() { return !$scope.max || $scope.getItems().length < $scope.max; }; $scope.canRemove = function() { return !$scope.min || $scope.getItems().length > $scope.min; }; } }; }) .directive('afRepeatItem', function() { return { restrict: 'A', require: { afRepeat: '^^', outerRepeatItem: '?^^afRepeatItem' }, bindToController: { item: '=afRepeatItem', repeatIndex: '=' }, controller: function() { this.getFieldData = function() { return this.afRepeat.getRepeatType() === 'join' ? this.item : this.item.fields; }; this.getEntityType = function() { return this.afRepeat.getEntityController().getEntityType(); }; } }; }); angular.module('af').component('afSearchParamSets', { require: { afFieldset: '^^' }, templateUrl: '~/af/afSearchParamSets.html', controller: function($scope, $element, crmApi4, $window, $location) { const ts = $scope.ts = CRM.ts('org.civicrm.afform'); this.savedSets = []; this.$onInit = () => { this.formName = this.afFieldset.getFormName(); this.saveDialog.reset(); this.fetchSearchParamSets(); }; this.fetchSearchParamSets = () => crmApi4('SearchParamSet', 'get', { select: ['label', 'filters', 'columns', 'created_by.display_name', 'created_date'], where: [['afform_name', '=', this.formName]], orderBy: {'label': 'ASC'} }) .then((results) => this.savedSets = results.map((paramSet) => { const created = new Date(paramSet.created_date); const createdDate = created.toLocaleDateString(); const isToday = createdDate === (new Date()).toLocaleDateString(); paramSet.created_date_or_time = isToday ? ts('%1 today', {1: created.toLocaleTimeString()}) : createdDate; return paramSet; })) .catch(() => this.savedSets = [{id: '', label: ts('Error fetching Saved Searches')}]); this.applySearchParamSet = (id) => $window.location.hash = `#?_s=${id ? id : 0}`; this.getSetSearchParamSet = (value) => { if (value !== undefined) { this.applySearchParamSet(value); return ''; } const selected = this.getSelectedParamSet(); return (selected && selected.id) ? `${selected.id}` : ''; }; this.getSelectedParamSet = () => { const hashParams = new URLSearchParams($window.location.hash.slice(1)); const urlValue = parseInt(hashParams.get('_s')); return this.savedSets.find((searchParamSet) => searchParamSet.id === urlValue); }; this.getCurrentParams = () => { if (!this.afFieldset) { return {}; } const params = {}; params.filters = this.afFieldset.getFilterValues(); params.columns = {}; $element.closest('[af-fieldset]').find('crm-search-display-table').each((i, $table) => { const tableCtrl = angular.element($table).controller('crmSearchDisplayTable'); if (!tableCtrl) { return; } const columns = tableCtrl.getToggledColumns(); if (!Object.keys(columns).length) { return; } params.columns[tableCtrl.getSearchDisplayKey()] = columns; }); return params; }; this.renderSearchParamSetDescription = (paramSet) => ts('Created %1 by %2', { 1: paramSet.created_date_or_time, 2: (paramSet['created_by.display_name'] ? paramSet['created_by.display_name'] : 'UNKNOWN') }); this.renderSearchParamSetDetails = (paramSet) => { const rendered = {}; if (paramSet.filters) { const fieldMeta = this.afFieldset.getFieldMeta(); Object.keys(paramSet.filters).forEach((key) => { const defn = fieldMeta[key]; const rawValue = paramSet.filters[key]; const formatValue = (v) => { if (v && v.map) { return v.map(formatValue).join(' OR '); } if (defn.options && defn.options.length) { const selected = defn.options.find((o) => o.id == v); if (selected) { return selected.label; } } switch (typeof v) { case 'number': case 'string': return v; } return Object.entries(v) .filter((e) => e[1]) .map((e) => `${e[0]} ${e[1]}`) .join(' AND '); }; rendered[defn.label] = formatValue(rawValue); }); } if (paramSet.columns) { const displayKeys = Object.keys(paramSet.columns); if (displayKeys.length > 1) displayKeys.forEach((displayKey) => { const label = ts('%1 columns', {1: displayKey}); const columns = Object.values(paramSet.columns[displayKey]).join(', '); rendered[label] = columns; }); else if (displayKeys.length === 1) { const displayKey = displayKeys[0]; rendered[ts('Columns')] = Object.values(paramSet.columns[displayKey]).join(', '); } } return rendered; }; this.saveDialog = { open: () => $element[0].querySelector('dialog.af-search-param-set-new').showModal(), close: () => $element[0].querySelector('dialog.af-search-param-set-new').close(), reset: () => { this.saveDialog.label = ''; this.saveDialog.inProgress = false; }, canOpen: () => { const params = this.getCurrentParams(); if (Object.keys(params.filters).length) { return true; } if (Object.keys(params.columns).length) { return true; } return false; }, canSave: () => !this.saveDialog.inProgress && this.saveDialog.label && Object.keys(this.getCurrentParams()).length, save: () => { if (!this.saveDialog.canSave()) { return; } this.saveDialog.inProgress = true; const current = this.getCurrentParams(); const values = { afform_name: this.formName, filters: current.filters, columns: current.columns, label: this.saveDialog.label, created_by: CRM.config.cid, }; let newId = null; return crmApi4('SearchParamSet', 'create', { values: values }) .then((result) => newId = (result && result[0]) ? result[0].id : null) .then(() => this.fetchSearchParamSets()) .then(() => this.applySearchParamSet(newId)) .then(() => this.saveDialog.close()) .catch((error) => { const errorMessage = (error && error.error_message) ? error.error_message : ts('Unknown error'); CRM.alert(ts('Error saving filters: %1', {1: errorMessage})); }) .finally(() => this.saveDialog.reset()); } }; this.manageDialog = { open: () => $element[0].querySelector('dialog.af-search-param-sets-manage').showModal(), close: () => $element[0].querySelector('dialog.af-search-param-sets-manage').close(), deleteItem: (searchParamSet) => { crmApi4('SearchParamSet', 'delete', { where: [['id', '=', searchParamSet.id]] }) .then(() => this.fetchSearchParamSets()); }, }; this.updateDialog = { id: null, label: '', open: () => { const selected = this.getSelectedParamSet(); this.updateDialog.id = selected.id; this.updateDialog.oldLabel = selected.label; this.updateDialog.newLabel = selected.label; this.updateDialog.valueComparison = this.updateDialog.getValueComparison(); $element[0].querySelector('dialog.af-search-param-set-update').showModal(); }, close: () => $element[0].querySelector('dialog.af-search-param-set-update').close(), inProgress: false, canOpen: () => { const selected = this.getSelectedParamSet(); if (!selected) { return false; } const newParams = this.getCurrentParams(); if (JSON.stringify(selected.filters) !== JSON.stringify(newParams.filters)) { return true; } if (JSON.stringify(selected.columns) !== JSON.stringify(newParams.columns)) { return true; } return false; }, canUpdate: () => this.updateDialog.id && this.updateDialog.newLabel, update: () => { if (!this.updateDialog.canUpdate()) { return; } this.updateDialog.inProgress = true; const newParams = this.getCurrentParams(); crmApi4('SearchParamSet', 'update', { where: [['id', '=', this.updateDialog.id]], values: { label: this.updateDialog.newLabel, filters: newParams.filters, columns: newParams.columns } }) .then(() => this.fetchSearchParamSets()) .then(() => this.updateDialog.inProgress = false) .then(() => this.updateDialog.close()); }, getValueComparison: () => { const current = this.getSelectedParamSet(); const oldValues = this.renderSearchParamSetDetails(current); const newValues = this.renderSearchParamSetDetails(this.getCurrentParams()); const oldKeys = Object.keys(oldValues); const newKeys = Object.keys(newValues); const comparison = {}; oldKeys.filter((key) => !newKeys.includes(key)).forEach((key) => comparison[key] = [oldValues[key], ts('[none]')]); oldKeys.filter((key) => newKeys.includes(key)).forEach((key) => { if (newValues[key] === oldValues[key]) { comparison[key] = [oldValues[key]]; return; } comparison[key] = [oldValues[key], newValues[key]]; }); newKeys.filter((key) => !oldKeys.includes(key)).forEach((key) => comparison[key] = [ts('[none]'), newValues[key]]); return comparison; } }; } }); angular.module('af').component('afTabset', { templateUrl: '~/af/afTabset.html', transclude: true, controller: function($scope, $element) { this.tabs = []; this.$onInit = function() { $element.addClass('crm-tabset'); }; this.addTab = function(tab) { tab.tabSelected = !this.tabs.length; this.tabs.push(tab); }; this.selectTab = function(tabIndex) { this.tabs.forEach(function(tab, index) { tab.tabSelected = index === tabIndex; }); }; } }); angular.module('af').directive('afTab', function() { return { restrict: 'E', require: '^afTabset', scope: { title: '@', icon: '@', count: '@', }, transclude: true, template: '
', link: function (scope, element, attrs, afTabsetCtrl) { afTabsetCtrl.addTab(scope); } }; }); angular.module('af').directive('afTitle', function() { return { restrict: 'A', bindToController: { title: '@afTitle' }, controller: function($scope, $element) { const ctrl = this; $scope.$watch(function() {return ctrl.title;}, function(text) { let tag = 'h4'; if ($element.is('fieldset')) { tag = 'legend'; } if ($element.is('details')) { tag = 'summary'; } let $title = $element.children(tag + '.af-title'); if (!$title.length) { $title = $('<' + tag + ' class="af-title" />').prependTo($element); } $title.text(text); }); } }; }); angular.module('afCore', CRM.angRequires('afCore')); angular.module('afCore').service('afCoreDirective', function($location, crmApi4, crmStatus, crmUiAlert) { return function(camelName, meta, d) { d.restrict = 'E'; d.scope = {}; d.scope.options = '='; d.link = { pre: function($scope, $el, $attr) { $scope.ts = CRM.ts(camelName); $scope.meta = meta; $scope.crmApi4 = crmApi4; $scope.crmStatus = crmStatus; $scope.crmUiAlert = crmUiAlert; $scope.crmUrl = CRM.url; $el.addClass('afform-directive'); const dialog = $el.closest('.ui-dialog-content'); if (!dialog.length) { $scope.$watch(function() {return $location.search();}, function(params) { $scope.routeParams = params; }); } else { $scope.routeParams = {}; if (typeof dialog.data('urlHash') === 'string' && dialog.data('urlHash').includes('?')) { const searchParams = new URLSearchParams(dialog.data('urlHash').split('?')[1]); searchParams.forEach(function(value, key) { $scope.routeParams[key] = value; }); } } $scope.$parent.afformTitle = meta.title; $scope.addTitle = function(addition) { $scope.$parent.afformTitle = addition + ' ' + meta.title; }; $scope.checkLinkPerm = function(permissionName, createdId) { if (permissionName.startsWith('manage own') && CRM.config.cid !== createdId) { permissionName = permissionName.replace('manage own', 'administer'); } return CRM.checkPerm(permissionName); }; } }; return d; }; }); angular.module('afCore').directive('afApi3Ctrl', function() { return { restrict: 'EA', scope: { afApi3Ctrl: '=', afApi3: '@', afApi3Refresh: '@', onRefresh: '@' }, controllerAs: 'afApi3Ctrl', controller: function($scope, $parse, crmThrottle, crmApi) { var ctrl = this; var parts = $parse($scope.afApi3)($scope.$parent); ctrl.entity = parts[0]; ctrl.action = parts[1]; ctrl.params = parts[2]; ctrl.result = {}; ctrl.loading = ctrl.firstLoad = true; ctrl.refresh = function refresh() { ctrl.loading = true; crmThrottle(function () { return crmApi(ctrl.entity, ctrl.action, ctrl.params) .then(function (response) { ctrl.result = response; ctrl.loading = ctrl.firstLoad = false; if ($scope.onRefresh) { $scope.$parent.$eval($scope.onRefresh, ctrl); } }); }); }; $scope.afApi3Ctrl = this; var mode = $scope.afApi3Refresh ? $scope.afApi3Refresh : 'auto'; switch (mode) { case 'auto': $scope.$watchCollection('afApi3Ctrl.params', ctrl.refresh); break; case 'init': ctrl.refresh(); break; case 'manual': break; default: throw 'Unrecognized refresh mode: '+ mode; } } }; }); angular.module('afCore').directive('afApi4Action', function($parse, crmStatus, crmApi4) { return { restrict: 'A', scope: { afApi4Action: '@', afApi4StartMsg: '=', afApi4ErrorMsg: '=', afApi4SuccessMsg: '=', afApi4Success: '@', onError: '@' }, link: function($scope, $el, $attr) { const ts = CRM.ts(null); function running(x) {$el.toggleClass('af-api4-action-running', x).toggleClass('af-api4-action-idle', !x);} running(false); $el.click(function(){ const parts = $parse($scope.afApi4Action)($scope.$parent); const msgs = { start: $scope.afApi4StartMsg || ts('Submitting...'), success: $scope.afApi4SuccessMsg, error: $scope.afApi4ErrorMsg }; running(true); crmStatus(msgs, crmApi4(parts[0], parts[1], parts[2])) .finally(function(){running(false);}) .then(function(response){$scope.$parent.$eval($scope.afApi4Success, {response: response});}) .catch(function(error){$scope.$parent.$eval($scope.onError, {error: error});}); }); } }; }); angular.module('afCore').directive('afApi4Ctrl', function() { return { restrict: 'EA', scope: { afApi4Ctrl: '=', afApi4: '@', afApi4Refresh: '@', onRefresh: '@' }, controllerAs: 'afApi4Ctrl', controller: function($scope, $parse, crmThrottle, crmApi4) { var ctrl = this; var parts = $parse($scope.afApi4)($scope.$parent); ctrl.entity = parts[0]; ctrl.action = parts[1]; ctrl.params = parts[2]; ctrl.index = parts[3]; ctrl.result = {}; ctrl.loading = ctrl.firstLoad = true; ctrl.refresh = function refresh() { ctrl.loading = true; crmThrottle(function () { return crmApi4(ctrl.entity, ctrl.action, ctrl.params, ctrl.index) .then(function (response) { ctrl.result = response; ctrl.loading = ctrl.firstLoad = false; if ($scope.onRefresh) { $scope.$parent.$eval($scope.onRefresh, ctrl); } }); }); }; $scope.afApi4Ctrl = this; var mode = $scope.afApi4Refresh ? $scope.afApi4Refresh : 'auto'; switch (mode) { case 'auto': $scope.$watchCollection('afApi4Ctrl.params', ctrl.refresh, true); $scope.$watch('afApi4Ctrl.index', ctrl.refresh, true); $scope.$watch('afApi4Ctrl.entity', ctrl.refresh, true); $scope.$watch('afApi4Ctrl.action', ctrl.refresh, true); break; case 'init': ctrl.refresh(); break; case 'manual': break; default: throw 'Unrecognized refresh mode: '+ mode; } } }; }); angular.module('afformRequestFinancialAssistanceForOurVolunteerPrograms1', CRM.angRequires('afformRequestFinancialAssistanceForOurVolunteerPrograms1')); angular.module('afformRequestFinancialAssistanceForOurVolunteerPrograms1').directive('afformRequestFinancialAssistanceForOurVolunteerPrograms1', function(afCoreDirective) { return afCoreDirective("afformRequestFinancialAssistanceForOurVolunteerPrograms1", {"title":"Volunteer Vacation Financial Assistance Request","redirect":null,"name":"afformRequestFinancialAssistanceForOurVolunteerPrograms1","autosave_draft":null,"confirmation_type":"redirect_to_url","confirmation_message":null}, { templateUrl: "~/afformRequestFinancialAssistanceForOurVolunteerPrograms1/afformRequestFinancialAssistanceForOurVolunteerPrograms1.aff.html" }); }); angular.module('afformStandalone', CRM.angular.modules) .controller('AfformStandalonePageCtrl', function($scope) { $scope.afformTitle = ''; }); })(angular, CRM.$, CRM._); /* angular-file-upload v2.6.1 https://github.com/nervgh/angular-file-upload */ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["angular-file-upload"]=t():e["angular-file-upload"]=t()}(this,function(){return function(e){function t(n){if(o[n])return o[n].exports;var r=o[n]={exports:{},id:n,loaded:!1};return e[n].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var o={};return t.m=e,t.c=o,t.p="",t(0)}([function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}var r=o(1),i=n(r),s=o(2),a=n(s),u=o(3),l=n(u),p=o(4),c=n(p),f=o(5),d=n(f),h=o(6),y=n(h),m=o(7),v=n(m),_=o(8),g=n(_),b=o(9),F=n(b),O=o(10),C=n(O),T=o(11),I=n(T),w=o(12),A=n(w),U=o(13),x=n(U);angular.module(i.default.name,[]).value("fileUploaderOptions",a.default).factory("FileUploader",l.default).factory("FileLikeObject",c.default).factory("FileItem",d.default).factory("FileDirective",y.default).factory("FileSelect",v.default).factory("FileDrop",F.default).factory("FileOver",C.default).factory("Pipeline",g.default).directive("nvFileSelect",I.default).directive("nvFileDrop",A.default).directive("nvFileOver",x.default).run(["FileUploader","FileLikeObject","FileItem","FileDirective","FileSelect","FileDrop","FileOver","Pipeline",function(e,t,o,n,r,i,s,a){e.FileLikeObject=t,e.FileItem=o,e.FileDirective=n,e.FileSelect=r,e.FileDrop=i,e.FileOver=s,e.Pipeline=a}])},function(e,t){e.exports={name:"angularFileUpload"}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={url:"/",alias:"file",headers:{},queue:[],progress:0,autoUpload:!1,removeAfterUpload:!1,method:"POST",filters:[],formData:[],queueLimit:Number.MAX_VALUE,withCredentials:!1,disableMultipart:!1}},function(e,t,o){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t,o,n,i,a,u,g){var b=n.File,F=n.FormData,O=function(){function n(t){r(this,n);var o=p(e);c(this,o,t,{isUploading:!1,_nextIndex:0,_directives:{select:[],drop:[],over:[]}}),this.filters.unshift({name:"queueLimit",fn:this._queueLimitFilter}),this.filters.unshift({name:"folder",fn:this._folderFilter})}return n.prototype.addToQueue=function(e,t,o){var n=this,r=this.isArrayLikeObject(e)?Array.prototype.slice.call(e):[e],i=this._getFilters(o),l=this.queue.length,p=[],c=function e(){var o=r.shift();if(v(o))return f();var l=n.isFile(o)?o:new a(o),c=n._convertFiltersToPipes(i),d=new g(c),h=function(t){var o=t.pipe.originalFilter,r=s(t.args,2),i=r[0],a=r[1];n._onWhenAddingFileFailed(i,o,a),e()},y=function(t,o){var r=new u(n,t,o);p.push(r),n.queue.push(r),n._onAfterAddingFile(r),e()};d.onThrown=h,d.onSuccessful=y,d.exec(l,t)},f=function(){n.queue.length!==l&&(n._onAfterAddingAll(p),n.progress=n._getTotalProgress()),n._render(),n.autoUpload&&n.uploadAll()};c()},n.prototype.removeFromQueue=function(e){var t=this.getIndexOfItem(e),o=this.queue[t];o.isUploading&&o.cancel(),this.queue.splice(t,1),o._destroy(),this.progress=this._getTotalProgress()},n.prototype.clearQueue=function(){for(;this.queue.length;)this.queue[0].remove();this.progress=0},n.prototype.uploadItem=function(e){var t=this.getIndexOfItem(e),o=this.queue[t],n=this.isHTML5?"_xhrTransport":"_iframeTransport";o._prepareToUploading(),this.isUploading||(this._onBeforeUploadItem(o),o.isCancel||(o.isUploading=!0,this.isUploading=!0,this[n](o),this._render()))},n.prototype.cancelItem=function(e){var t=this,o=this.getIndexOfItem(e),n=this.queue[o],r=this.isHTML5?"_xhr":"_form";if(n)if(n.isCancel=!0,n.isUploading)n[r].abort();else{var s=[void 0,0,{}],a=function(){t._onCancelItem.apply(t,[n].concat(s)),t._onCompleteItem.apply(t,[n].concat(s))};i(a)}},n.prototype.uploadAll=function(){var e=this.getNotUploadedItems().filter(function(e){return!e.isUploading});e.length&&(f(e,function(e){return e._prepareToUploading()}),e[0].upload())},n.prototype.cancelAll=function(){var e=this.getNotUploadedItems();f(e,function(e){return e.cancel()})},n.prototype.isFile=function(e){return this.constructor.isFile(e)},n.prototype.isFileLikeObject=function(e){return this.constructor.isFileLikeObject(e)},n.prototype.isArrayLikeObject=function(e){return this.constructor.isArrayLikeObject(e)},n.prototype.getIndexOfItem=function(e){return h(e)?e:this.queue.indexOf(e)},n.prototype.getNotUploadedItems=function(){return this.queue.filter(function(e){return!e.isUploaded})},n.prototype.getReadyItems=function(){return this.queue.filter(function(e){return e.isReady&&!e.isUploading}).sort(function(e,t){return e.index-t.index})},n.prototype.destroy=function(){var e=this;f(this._directives,function(t){f(e._directives[t],function(e){e.destroy()})})},n.prototype.onAfterAddingAll=function(e){},n.prototype.onAfterAddingFile=function(e){},n.prototype.onWhenAddingFileFailed=function(e,t,o){},n.prototype.onBeforeUploadItem=function(e){},n.prototype.onProgressItem=function(e,t){},n.prototype.onProgressAll=function(e){},n.prototype.onSuccessItem=function(e,t,o,n){},n.prototype.onErrorItem=function(e,t,o,n){},n.prototype.onCancelItem=function(e,t,o,n){},n.prototype.onCompleteItem=function(e,t,o,n){},n.prototype.onTimeoutItem=function(e){},n.prototype.onCompleteAll=function(){},n.prototype._getTotalProgress=function(e){if(this.removeAfterUpload)return e||0;var t=this.getNotUploadedItems().length,o=t?this.queue.length-t:this.queue.length,n=100/this.queue.length,r=(e||0)*n/100;return Math.round(o*n+r)},n.prototype._getFilters=function(e){if(!e)return this.filters;if(m(e))return e;var t=e.match(/[^\s,]+/g);return this.filters.filter(function(e){return t.indexOf(e.name)!==-1})},n.prototype._convertFiltersToPipes=function(e){var t=this;return e.map(function(e){var o=l(t,e.fn);return o.isAsync=3===e.fn.length,o.originalFilter=e,o})},n.prototype._render=function(){t.$$phase||t.$apply()},n.prototype._folderFilter=function(e){return!(!e.size&&!e.type)},n.prototype._queueLimitFilter=function(){return this.queue.length=200&&e<300||304===e},n.prototype._transformResponse=function(e,t){var n=this._headersGetter(t);return f(o.defaults.transformResponse,function(t){e=t(e,n)}),e},n.prototype._parseHeaders=function(e){var t,o,n,r={};return e?(f(e.split("\n"),function(e){n=e.indexOf(":"),t=e.slice(0,n).trim().toLowerCase(),o=e.slice(n+1).trim(),t&&(r[t]=r[t]?r[t]+", "+o:o)}),r):r},n.prototype._headersGetter=function(e){return function(t){return t?e[t.toLowerCase()]||null:e}},n.prototype._xhrTransport=function(e){var t,o=this,n=e._xhr=new XMLHttpRequest;if(e.disableMultipart?t=e._file:(t=new F,f(e.formData,function(e){f(e,function(e,o){t.append(o,e)})}),t.append(e.alias,e._file,e.file.name)),"number"!=typeof e._file.size)throw new TypeError("The file specified is no longer valid");n.upload.onprogress=function(t){var n=Math.round(t.lengthComputable?100*t.loaded/t.total:0);o._onProgressItem(e,n)},n.onload=function(){var t=o._parseHeaders(n.getAllResponseHeaders()),r=o._transformResponse(n.response,t),i=o._isSuccessCode(n.status)?"Success":"Error",s="_on"+i+"Item";o[s](e,r,n.status,t),o._onCompleteItem(e,r,n.status,t)},n.onerror=function(){var t=o._parseHeaders(n.getAllResponseHeaders()),r=o._transformResponse(n.response,t);o._onErrorItem(e,r,n.status,t),o._onCompleteItem(e,r,n.status,t)},n.onabort=function(){var t=o._parseHeaders(n.getAllResponseHeaders()),r=o._transformResponse(n.response,t);o._onCancelItem(e,r,n.status,t),o._onCompleteItem(e,r,n.status,t)},n.ontimeout=function(t){var r=o._parseHeaders(n.getAllResponseHeaders()),i="Request Timeout.";o._onTimeoutItem(e),o._onCompleteItem(e,i,408,r)},n.open(e.method,e.url,!0),n.timeout=e.timeout||0,n.withCredentials=e.withCredentials,f(e.headers,function(e,t){n.setRequestHeader(t,e)}),n.send(t)},n.prototype._iframeTransport=function(e){var t=this,o=_('
'),n=_('