require_once 'afform.civix.php';
use CRM_Afform_ExtensionUtil as E;
-
-function _afform_fields() {
- return ['name', 'title', 'description', 'requires', 'layout', 'server_route', 'client_route', 'is_public'];
-}
+use Civi\Api4\Action\Afform\Submit;
/**
* Filter the content of $params to only have supported afform fields.
*/
function _afform_fields_filter($params) {
$result = [];
- foreach (_afform_fields() as $field) {
- if (isset($params[$field])) {
- $result[$field] = $params[$field];
+ $fields = \Civi\Api4\Afform::getfields()->setCheckPermissions(FALSE)->execute()->indexBy('name');
+ foreach ($fields as $fieldName => $field) {
+ if (isset($params[$fieldName])) {
+ $result[$fieldName] = $params[$fieldName];
}
- if (isset($result[$field])) {
- switch ($field) {
+ if (isset($result[$fieldName])) {
+ switch ($fieldName) {
case 'is_public':
- $result[$field] = CRM_Utils_String::strtobool($result[$field]);
+ $result[$fieldName] = CRM_Utils_String::strtobool($result[$fieldName]);
break;
}
* @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
*/
function afform_civicrm_container($container) {
+ $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
$container->setDefinition('afform_scanner', new \Symfony\Component\DependencyInjection\Definition(
'CRM_Afform_AfformScanner',
[]
*/
function afform_civicrm_config(&$config) {
_afform_civix_civicrm_config($config);
+
+ if (isset(Civi::$statics[__FUNCTION__])) {
+ return;
+ }
+ Civi::$statics[__FUNCTION__] = 1;
+
+ // Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processContacts'], -500);
+ Civi::dispatcher()->addListener(Submit::EVENT_NAME, [Submit::class, 'processGenericEntity'], -1000);
+ Civi::dispatcher()->addListener('hook_civicrm_angularModules', '_afform_civicrm_angularModules_autoReq', -1000);
}
/**
function afform_civicrm_angularModules(&$angularModules) {
_afform_civix_civicrm_angularModules($angularModules);
+ /** @var CRM_Afform_AfformScanner $scanner */
$scanner = Civi::service('afform_scanner');
$names = array_keys($scanner->findFilePaths());
foreach ($names as $name) {
'js' => ['assetBuilder://afform.js?name=' . urlencode($name)],
'requires' => $meta['requires'],
'basePages' => [],
+ 'exports' => [
+ _afform_angular_module_name($name, 'dash') => 'AE',
+ ],
];
// FIXME: The HTML layout template is embedded in the JS asset.
}
}
+/**
+ * Scan the list of Angular modules and inject automatic-requirements.
+ *
+ * TLDR: if an afform uses element "<other-el/>", and if another module defines
+ * `$angularModules['otherMod']['exports']['el'][0] === 'other-el'`, then
+ * the 'otherMod' is automatically required.
+ *
+ * @param \Civi\Core\Event\GenericHookEvent $e
+ * @see CRM_Utils_Hook::angularModules()
+ */
+function _afform_civicrm_angularModules_autoReq($e) {
+ /** @var CRM_Afform_AfformScanner $scanner */
+ $scanner = Civi::service('afform_scanner');
+ $moduleEnvId = md5(\CRM_Core_Config_Runtime::getId() . implode(',', array_keys($e->angularModules)));
+ $depCache = CRM_Utils_Cache::create([
+ 'name' => 'afdep_' . substr($moduleEnvId, 0, 32 - 6),
+ 'type' => ['*memory*', 'SqlGroup', 'ArrayCache'],
+ 'withArray' => 'fast',
+ 'prefetch' => TRUE,
+ ]);
+ $depCacheTtl = 2 * 60 * 60;
+
+ $revMap = _afform_reverse_deps($e->angularModules);
+
+ $formNames = array_keys($scanner->findFilePaths());
+ foreach ($formNames as $formName) {
+ $angModule = _afform_angular_module_name($formName, 'camel');
+ $cacheLine = $depCache->get($formName, NULL);
+
+ $jFile = $scanner->findFilePath($formName, 'aff.json');
+ $hFile = $scanner->findFilePath($formName, 'aff.html');
+
+ $jStat = stat($jFile);
+ $hStat = stat($hFile);
+
+ if ($cacheLine === NULL) {
+ $needsUpdate = TRUE;
+ }
+ elseif ($jStat !== FALSE && $jStat['size'] !== $cacheLine['js']) {
+ $needsUpdate = TRUE;
+ }
+ elseif ($jStat !== FALSE && $jStat['mtime'] > $cacheLine['jm']) {
+ $needsUpdate = TRUE;
+ }
+ elseif ($hStat !== FALSE && $hStat['size'] !== $cacheLine['hs']) {
+ $needsUpdate = TRUE;
+ }
+ elseif ($hStat !== FALSE && $hStat['mtime'] > $cacheLine['hm']) {
+ $needsUpdate = TRUE;
+ }
+ else {
+ $needsUpdate = FALSE;
+ }
+
+ if ($needsUpdate) {
+ $cacheLine = [
+ 'js' => $jStat['size'] ?? NULL,
+ 'jm' => $jStat['mtime'] ?? NULL,
+ 'hs' => $hStat['size'] ?? NULL,
+ 'hm' => $hStat['mtime'] ?? NULL,
+ 'r' => array_values(array_unique(array_merge(
+ [CRM_Afform_AfformScanner::DEFAULT_REQUIRES],
+ $e->angularModules[$angModule]['requires'] ?? [],
+ _afform_reverse_deps_find($formName, file_get_contents($hFile), $revMap)
+ ))),
+ ];
+ // print_r(['cache update:' . $formName => $cacheLine]);
+ $depCache->set($formName, $cacheLine, $depCacheTtl);
+ }
+
+ $e->angularModules[$angModule]['requires'] = $cacheLine['r'];
+ }
+}
+
+/**
+ * @param $angularModules
+ * @return array
+ * 'attr': array(string $attrName => string $angModuleName)
+ * 'el': array(string $elementName => string $angModuleName)
+ */
+function _afform_reverse_deps($angularModules) {
+ $revMap = ['attr' => [], 'el' => []];
+ foreach (array_keys($angularModules) as $module) {
+ if (!isset($angularModules[$module]['exports'])) {
+ continue;
+ }
+ foreach ($angularModules[$module]['exports'] as $symbolName => $symbolTypes) {
+ if (strpos($symbolTypes, 'A') !== FALSE) {
+ $revMap['attr'][$symbolName] = $module;
+ }
+ if (strpos($symbolTypes, 'E') !== FALSE) {
+ $revMap['el'][$symbolName] = $module;
+ }
+ }
+ }
+ return $revMap;
+}
+
+/**
+ * @param string $formName
+ * @param string $html
+ * @param array $revMap
+ * The reverse-dependencies map from _afform_reverse_deps().
+ * @return array
+ * @see _afform_reverse_deps()
+ */
+function _afform_reverse_deps_find($formName, $html, $revMap) {
+ $symbols = \Civi\Afform\Symbols::scan($html);
+ $elems = array_intersect_key($revMap['el'], $symbols->elements);
+ $attrs = array_intersect_key($revMap['attr'], $symbols->attributes);
+ return array_values(array_unique(array_merge($elems, $attrs)));
+}
+
/**
* @param \Civi\Angular\Manager $angular
* @see CRM_Utils_Hook::alterAngular()
foreach (pq('af-field', $doc) as $afField) {
/** @var DOMElement $afField */
$fieldName = $afField->getAttribute('name');
- $entityName = pq($afField)->parent('af-fieldset[model]')->attr('model');
+ $entityName = pq($afField)->parents('[af-fieldset]')->attr('af-fieldset');
if (!preg_match(';^[a-zA-Z0-9\_\-\. ]+$;', $entityName)) {
throw new \CRM_Core_Exception("Cannot process $path: malformed entity name ($entityName)");
}
$entityType = $entities[$entityName]['type'];
$getFields = civicrm_api4($entityType, 'getFields', [
+ 'action' => 'create',
'where' => [['name', '=', $fieldName]],
'select' => ['title', 'input_type', 'input_attrs', 'options'],
'loadOptions' => TRUE,
]);
// Merge field definition data with whatever's already in the markup
- foreach ($getFields as $field) {
+ $deep = ['input_attrs'];
+ foreach ($getFields as $fieldInfo) {
$existingFieldDefn = trim(pq($afField)->attr('defn') ?: '');
if ($existingFieldDefn && $existingFieldDefn[0] != '{') {
// If it's not an object, don't mess with it.
continue;
}
- foreach ($field as &$prop) {
- $prop = json_encode($prop, JSON_UNESCAPED_SLASHES);
+ // TODO: Teach the api to return options in this format
+ if (!empty($fieldInfo['options'])) {
+ $fieldInfo['options'] = CRM_Utils_Array::makeNonAssociative($fieldInfo['options'], 'key', 'label');
}
- if ($existingFieldDefn) {
- $field = array_merge($field, CRM_Utils_JS::getRawProps($existingFieldDefn));
+ // Default placeholder for select inputs
+ if ($fieldInfo['input_type'] === 'Select') {
+ $fieldInfo['input_attrs'] = ($fieldInfo['input_attrs'] ?? []) + ['placeholder' => ts('Select')];
}
- pq($afField)->attr('defn', CRM_Utils_JS::writeObject($field));
+
+ $fieldDefn = $existingFieldDefn ? CRM_Utils_JS::getRawProps($existingFieldDefn) : [];
+ foreach ($fieldInfo as $name => $prop) {
+ // Merge array props 1 level deep
+ if (in_array($name, $deep) && !empty($fieldDefn[$name])) {
+ $fieldDefn[$name] = CRM_Utils_JS::writeObject(CRM_Utils_JS::getRawProps($fieldDefn[$name]) + array_map(['CRM_Utils_JS', 'encode'], $prop));
+ }
+ elseif (!isset($fieldDefn[$name])) {
+ $fieldDefn[$name] = CRM_Utils_JS::encode($prop);
+ }
+ }
+ pq($afField)->attr('defn', htmlspecialchars(CRM_Utils_JS::writeObject($fieldDefn)));
}
}
});
function _afform_getMetadata(phpQueryObject $doc) {
$entities = [];
- foreach ($doc->find('af-model') as $afmModelProp) {
+ foreach ($doc->find('af-entity') as $afmModelProp) {
$entities[$afmModelProp->getAttribute('name')] = [
'type' => $afmModelProp->getAttribute('type'),
];
_afform_civix_civicrm_entityTypes($entityTypes);
}
+/**
+ * Implements hook_civicrm_themes().
+ */
+function afform_civicrm_themes(&$themes) {
+ _afform_civix_civicrm_themes($themes);
+}
+
// --- Functions below this ship commented out. Uncomment as required. ---
/**
}
$name = $params['name'];
- // Hmm?? $scanner = new CRM_Afform_AfformScanner();
- // Hmm?? afform_scanner
+ /** @var \CRM_Afform_AfformScanner $scanner */
$scanner = Civi::service('afform_scanner');
$meta = $scanner->getMeta($name);
- // Hmm?? $scanner = new CRM_Afform_AfformScanner();
-
- $fileName = '~afform/' . _afform_angular_module_name($name, 'camel');
- $htmls = [
- $fileName => file_get_contents($scanner->findFilePath($name, 'aff.html')),
- ];
- $htmls = \Civi\Angular\ChangeSet::applyResourceFilters(Civi::service('angular')->getChangeSets(), 'partials', $htmls);
$smarty = CRM_Core_Smarty::singleton();
$smarty->assign('afform', [
'camel' => _afform_angular_module_name($name, 'camel'),
'meta' => $meta,
'metaJson' => json_encode($meta),
- 'layout' => $htmls[$fileName],
+ 'layout' => _afform_html_filter($name, $scanner->getLayout($name)),
]);
$mimeType = 'text/javascript';
$content = $smarty->fetch('afform/AfformAngularModule.tpl');
}
+/**
+ * Apply any filters to an HTML partial.
+ *
+ * @param string $formName
+ * @param string $html
+ * Original HTML.
+ * @return string
+ * Modified HTML.
+ */
+function _afform_html_filter($formName, $html) {
+ $fileName = '~afform/' . _afform_angular_module_name($formName, 'camel');
+ $htmls = [$fileName => $html];
+ $htmls = \Civi\Angular\ChangeSet::applyResourceFilters(Civi::service('angular')->getChangeSets(), 'partials', $htmls);
+ return $htmls[$fileName];
+}
+
/**
* Implements hook_civicrm_alterMenu().
*/
}
}
+/**
+ * Clear any local/in-memory caches based on afform data.
+ */
+function _afform_clear() {
+ $container = \Civi::container();
+ $container->get('afform_scanner')->clear();
+
+ // Civi\Angular\Manager doesn't currently have a way to clear its in-memory
+ // data, so we just reset the whole object.
+ $container->set('angular', NULL);
+}
+
/**
* @param string $fileBaseName
* Ex: foo-bar