Afform - refactor admin page to use routing, add tabbed UI
authorColeman Watts <coleman@civicrm.org>
Wed, 13 Jan 2021 15:21:30 +0000 (10:21 -0500)
committerColeman Watts <coleman@civicrm.org>
Tue, 19 Jan 2021 17:23:18 +0000 (12:23 -0500)
24 files changed:
ext/afform/admin/CRM/AfformAdmin/Page/Base.php [new file with mode: 0644]
ext/afform/admin/CRM/AfformAdmin/Utils.php
ext/afform/admin/ang/afAdmin.aff.html [deleted file]
ext/afform/admin/ang/afAdmin.aff.json [deleted file]
ext/afform/admin/ang/afAdmin.ang.php [new file with mode: 0644]
ext/afform/admin/ang/afAdmin.js [new file with mode: 0644]
ext/afform/admin/ang/afAdmin/afAdminGui.controller.js [new file with mode: 0644]
ext/afform/admin/ang/afAdmin/afAdminList.controller.js [new file with mode: 0644]
ext/afform/admin/ang/afAdmin/afAdminList.html [new file with mode: 0644]
ext/afform/admin/ang/afAdminList.aff.html [deleted file]
ext/afform/admin/ang/afGuiEditor.ang.php
ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
ext/afform/admin/ang/afGuiEditor/afGuiEditor.html
ext/afform/admin/templates/CRM/AfformAdmin/Page/Base.tpl [new file with mode: 0644]
ext/afform/admin/xml/Menu/afform_admin.xml [new file with mode: 0644]
ext/afform/core/Civi/Api4/Afform.php
ext/afform/core/ang/afblockNameHousehold.aff.json
ext/afform/core/ang/afblockNameIndividual.aff.json
ext/afform/core/ang/afblockNameOrganization.aff.json
ext/afform/core/ang/afjoinAddressDefault.aff.json
ext/afform/core/ang/afjoinEmailDefault.aff.json
ext/afform/core/ang/afjoinIMDefault.aff.json
ext/afform/core/ang/afjoinPhoneDefault.aff.json
ext/afform/core/ang/afjoinWebsiteDefault.aff.json

diff --git a/ext/afform/admin/CRM/AfformAdmin/Page/Base.php b/ext/afform/admin/CRM/AfformAdmin/Page/Base.php
new file mode 100644 (file)
index 0000000..31338e5
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Base page for Afform admin
+ */
+class CRM_AfformAdmin_Page_Base extends CRM_Core_Page {
+
+  public function run() {
+    $breadCrumb = [
+      'title' => ts('Forms'),
+      'url' => CRM_Utils_System::url('civicrm/admin/afform', NULL, FALSE, '/'),
+    ];
+    CRM_Utils_System::appendBreadCrumb([$breadCrumb]);
+
+    // Load angular module
+    $loader = new Civi\Angular\AngularLoader();
+    $loader->setPageName('civicrm/admin/afform');
+    $loader->useApp();
+    $loader->load();
+    parent::run();
+  }
+
+}
index 00cf47ba82b53795fd900b51c90dfad46d9b2547..5233a295d112efdca41045cd51122bf3edec0ef1 100644 (file)
@@ -3,12 +3,27 @@ use CRM_AfformAdmin_ExtensionUtil as E;
 
 class CRM_AfformAdmin_Utils {
 
+  /**
+   * @return array
+   */
+  public static function getAdminSettings() {
+    return [
+      'afform_type' => \Civi\Api4\OptionValue::get(FALSE)
+        ->addSelect('name', 'label', 'icon')
+        ->addWhere('is_active', '=', TRUE)
+        ->addWhere('option_group_id:name', '=', 'afform_type')
+        ->addOrderBy('weight', 'ASC')
+        ->execute(),
+    ];
+  }
+
   /**
    * Loads metadata for the gui editor.
    *
    * FIXME: This is a prototype and should get broken out into separate callbacks with hooks, events, etc.
+   * @return array
    */
-  public static function getAngularSettings() {
+  public static function getGuiSettings() {
     $getFieldParams = [
       'checkPermissions' => FALSE,
       'includeCustom' => TRUE,
diff --git a/ext/afform/admin/ang/afAdmin.aff.html b/ext/afform/admin/ang/afAdmin.aff.html
deleted file mode 100644 (file)
index ad443fe..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-<div ng-if="!routeParams.name">
-  <af-admin-list></af-admin-list>
-</div>
-
-<div ng-if="routeParams.name">
-  <af-gui-editor name="routeParams.name"></af-gui-editor>
-</div>
diff --git a/ext/afform/admin/ang/afAdmin.aff.json b/ext/afform/admin/ang/afAdmin.aff.json
deleted file mode 100644 (file)
index 22078f8..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "title": "Afform Administration",
-  "server_route": "civicrm/admin/afform",
-  "permission": "administer CiviCRM",
-  "requires": [
-    "afGuiEditor"
-  ]
-}
diff --git a/ext/afform/admin/ang/afAdmin.ang.php b/ext/afform/admin/ang/afAdmin.ang.php
new file mode 100644 (file)
index 0000000..818a6f4
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+// Angular module for afform gui editor
+return [
+  'js' => [
+    'ang/afAdmin.js',
+    'ang/afAdmin/*.js',
+    'ang/afAdmin/*/*.js',
+  ],
+  'css' => [],
+  'partials' => ['ang/afAdmin'],
+  'requires' => ['api4', 'afGuiEditor', 'crmRouteBinder'],
+  'settingsFactory' => ['CRM_AfformAdmin_Utils', 'getAdminSettings'],
+  'basePages' => ['civicrm/admin/afform'],
+  'bundles' => ['bootstrap3'],
+];
diff --git a/ext/afform/admin/ang/afAdmin.js b/ext/afform/admin/ang/afAdmin.js
new file mode 100644 (file)
index 0000000..1284b00
--- /dev/null
@@ -0,0 +1,30 @@
+(function(angular, $, _) {
+  "use strict";
+  angular.module('afAdmin', CRM.angRequires('afAdmin'))
+
+    .config(function($routeProvider) {
+      $routeProvider.when('/', {
+        controller: 'afAdminList',
+        reloadOnSearch: false,
+        templateUrl: '~/afAdmin/afAdminList.html',
+        resolve: {
+          // Load data for lists
+          afforms: function(crmApi4) {
+            return crmApi4('Afform', 'get', {
+              select: ['name', 'title', 'type', 'is_public', 'server_route', 'has_local', 'has_base'],
+              orderBy: {title: 'ASC'}
+            });
+          }
+        }
+      });
+      $routeProvider.when('/create/:type', {
+        controller: 'afAdminGui',
+        template: '<af-gui-editor type="$ctrl.type"></af-gui-editor>',
+      });
+      $routeProvider.when('/edit/:name', {
+        controller: 'afAdminGui',
+        template: '<af-gui-editor name="$ctrl.name"></af-gui-editor>',
+      });
+    });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/afform/admin/ang/afAdmin/afAdminGui.controller.js b/ext/afform/admin/ang/afAdmin/afAdminGui.controller.js
new file mode 100644 (file)
index 0000000..a7713e3
--- /dev/null
@@ -0,0 +1,15 @@
+(function(angular, $, _) {
+  "use strict";
+
+  angular.module('afAdmin').controller('afAdminGui', function($scope, $routeParams) {
+    var ts = $scope.ts = CRM.ts(),
+      ctrl = $scope.$ctrl = this;
+
+    // Edit mode
+    this.name = $routeParams.name;
+    // Create mode
+    this.type = $routeParams.type;
+
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/afform/admin/ang/afAdmin/afAdminList.controller.js b/ext/afform/admin/ang/afAdmin/afAdminList.controller.js
new file mode 100644 (file)
index 0000000..682b17a
--- /dev/null
@@ -0,0 +1,51 @@
+(function(angular, $, _) {
+  "use strict";
+
+  angular.module('afAdmin').controller('afAdminList', function($scope, afforms, crmApi4, crmStatus) {
+    var ts = $scope.ts = CRM.ts(),
+      ctrl = $scope.$ctrl = this;
+
+    $scope.crmUrl = CRM.url;
+
+    this.tabs = CRM.afAdmin.afform_type;
+
+    this.afforms = _.transform(afforms, function(afforms, afform) {
+      var type = afform.type || 'system';
+      afforms[type] = afforms[type] || [];
+      afforms[type].push(afform);
+    }, {});
+
+    $scope.$bindToRoute({
+      expr: '$ctrl.tab',
+      param: 'tab',
+      format: 'raw',
+      default: ctrl.tabs[0].name
+    });
+
+    this.revert = function(afform) {
+      var index = _.findIndex(ctrl.afforms[ctrl.tab], {name: afform.name});
+      if (index > -1) {
+        var apiOps = [['Afform', 'revert', {where: [['name', '=', afform.name]]}]];
+        if (afform.has_base) {
+          apiOps.push(['Afform', 'get', {
+            where: [['name', '=', afform.name]],
+            select: ['name', 'title', 'type', 'is_public', 'server_route', 'has_local', 'has_base']
+          }, 0]);
+        }
+        var apiCall = crmStatus(
+          afform.has_base ? {start: ts('Reverting...')} : {start: ts('Deleting...'), success: ts('Deleted')},
+          crmApi4(apiOps)
+        );
+        if (afform.has_base) {
+          afform.has_local = false;
+          apiCall.then(function(result) {
+            ctrl.afforms[ctrl.tab][index] = result[1];
+          });
+        } else {
+          ctrl.afforms[ctrl.tab].splice(index, 1);
+        }
+      }
+    };
+  });
+
+})(angular, CRM.$, CRM._);
diff --git a/ext/afform/admin/ang/afAdmin/afAdminList.html b/ext/afform/admin/ang/afAdmin/afAdminList.html
new file mode 100644 (file)
index 0000000..80af2d8
--- /dev/null
@@ -0,0 +1,59 @@
+<div id="bootstrap-theme" class="afadmin-list">
+  <h1 crm-page-title>{{:: ts('Configurable Forms') }}</h1>
+
+  <div class="form-inline text-right">
+    <a class="btn btn-primary" href="#/create/form">
+      <i class="crm-i fa-plus"></i>
+      {{:: ts('New Form') }}
+    </a>
+  </div>
+
+  <ul class="nav nav-tabs">
+    <li role="presentation" ng-repeat="tab in $ctrl.tabs" ng-class="{active: tab.name === $ctrl.tab}">
+      <a href ng-click="$ctrl.tab = tab.name"><i class="crm-i {{ tab.icon }}"></i> {{:: tab.label }}</a>
+    </li>
+  </ul>
+
+  <div class="form-inline">
+    <input class="form-control" type="search" ng-model="$ctrl.search" placeholder="{{:: ts('Filter') }}">
+  </div>
+
+  <table>
+    <thead>
+    <tr>
+      <th>{{:: ts('Title') }}</th>
+      <th>{{:: ts('Name') }}</th>
+      <th>{{:: ts('Server Route') }}</th>
+      <th>{{:: ts('Frontend?') }}</th>
+      <th></th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr ng-repeat="afform in $ctrl.afforms[$ctrl.tab] | filter:$ctrl.search">
+      <td>{{afform.title}}</td>
+      <td>
+        <code>{{afform.name}}</code>
+      </td>
+      <td>
+        <a ng-if="afform.server_route" ng-href="{{ crmUrl(afform.server_route) }}" target="_blank">
+          <code>{{afform.server_route}}</code>
+        </a>
+      </td>
+      <td>{{afform.is_public ? ts('Frontend') : ts('Backend')}}</td>
+      <td>
+        <a href="#/edit/{{ afform.name }}" class="btn btn-xs btn-primary">{{ ts('Edit') }}</a>
+        <a href ng-if="afform.has_local" class="btn btn-xs btn-danger" crm-confirm="{type: afform.has_base ? 'revert' : 'delete', obj: afform}" on-yes="$ctrl.revert(afform)">
+          {{ afform.has_base ? ts('Revert') : ts('Delete') }}
+        </a>
+      </td>
+    </tr>
+    <tr ng-if="!$ctrl.afforms[$ctrl.tab] || $ctrl.afforms[$ctrl.tab].length === 0">
+      <td colspan="9">
+        <p class="messages status no-popup text-center">
+          {{:: ts('None Found')}}
+        </p>
+      </td>
+    </tr>
+    </tbody>
+  </table>
+</div>
diff --git a/ext/afform/admin/ang/afAdminList.aff.html b/ext/afform/admin/ang/afAdminList.aff.html
deleted file mode 100644 (file)
index 8f572b4..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<a href="#!/?name=0" class="btn btn-default">
-  <i class="crm-i fa-plus"></i> {{:: ts('New Form') }}
-</a>
-<div
-  af-api4="['Afform', 'get', {select: ['name','title','is_public','server_route','has_local','has_base'], orderBy: {name:'ASC'}}]"
-  af-api4-ctrl="listCtrl">
-
-  <div ng-if="apiData.result.length == 0">
-    {{:: ts('None found.') }}
-  </div>
-
-  <table>
-    <thead>
-      <tr>
-        <th>{{:: ts('Name') }}</th>
-        <th>{{:: ts('Title') }}</th>
-        <th>{{:: ts('Server Route') }}</th>
-        <th>{{:: ts('Frontend?') }}</th>
-        <th></th>
-      </tr>
-    </thead>
-    <tbody>
-    <tr ng-repeat="availForm in listCtrl.result">
-      <td>
-        <a ng-href="#!/?name={{availForm.name}}">{{availForm.name}}</a>
-      </td>
-      <td>{{availForm.title}}</td>
-      <td>
-        <a ng-if="availForm.server_route" ng-href="{{crmUrl(availForm.server_route)}}" target="_blank">
-          <code>{{availForm.server_route}}</code>
-        </a>
-      </td>
-      <td>{{availForm.is_public ? ts('Frontend') : ts('Backend')}}</td>
-      <td>
-        <!--<a ng-click="crmStatus({start: ts('Reverting...'), success: ts('Reverted')}, crmApi4('Afform', 'revert', {where: [['name', '=', availForm.name]]}))">{{ts('Revert')}}</a>-->
-        <a af-api4-action="['Afform', 'revert', {where: [['name','=', availForm.name]]}]"
-           af-api4-start-msg="ts('Reverting...')"
-           af-api4-success-msg="ts('Reverted')"
-           af-api4-success="listCtrl.refresh()"
-           class="btn btn-xs btn-default"
-           ng-if="availForm.has_local && availForm.has_base"
-          >{{:: ts('Revert') }}</a>
-        <a af-api4-action="['Afform', 'revert', {where: [['name','=', availForm.name]]}]"
-           af-api4-start-msg="ts('Deleting...')"
-           af-api4-success-msg="ts('Deleted')"
-           af-api4-success="listCtrl.refresh()"
-           class="btn btn-xs btn-default"
-           ng-if="availForm.has_local && !availForm.has_base"
-        >{{:: ts('Delete') }}</a>
-      </td>
-    </tr>
-    </tbody>
-  </table>
-
-</div>
index 99c6a5e70a21bef43e85431720f4f26f00e301e6..4cca7330668125b4106e5e69c265efe8d8782443 100644 (file)
@@ -9,7 +9,7 @@ return [
   'css' => ['ang/afGuiEditor.css'],
   'partials' => ['ang/afGuiEditor'],
   'requires' => ['crmUi', 'crmUtil', 'dialogService', 'api4', 'crmMonaco', 'ui.sortable'],
-  'settingsFactory' => ['CRM_AfformAdmin_Utils', 'getAngularSettings'],
+  'settingsFactory' => ['CRM_AfformAdmin_Utils', 'getGuiSettings'],
   'basePages' => [],
   'exports' => [
     'af-gui-editor' => 'E',
index 428b903684f23f6945391d2ebbce8bbf9acabce5..5d43f11f4a3a4db1ce6401c1013a480ebd777e15 100644 (file)
@@ -5,6 +5,7 @@
   angular.module('afGuiEditor').component('afGuiEditor', {
     templateUrl: '~/afGuiEditor/afGuiEditor.html',
     bindings: {
+      type: '<',
       name: '<'
     },
     controllerAs: 'editor',
@@ -18,6 +19,7 @@
       var newForm = {
         title: '',
         permission: 'access CiviCRM',
+        type: 'form',
         layout: [{
           '#tag': 'af-form',
           ctrl: 'afform',
@@ -36,7 +38,7 @@
         $scope.afform = _.findWhere(afforms, {name: editor.name});
         if (!$scope.afform) {
           $scope.afform = _.cloneDeep(newForm);
-          if (editor.name != '0') {
+          if (editor.name) {
             alert('Error: unknown form "' + editor.name + '"');
           }
         }
         editor.layout = afGui.findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0];
         $scope.entities = afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name');
 
-        if (editor.name == '0') {
+        if (!editor.name) {
           editor.addEntity('Individual');
           editor.layout['#children'].push(afGui.meta.elements.submit.element);
         }
 
         // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
-        $scope.changesSaved = editor.name == '0' ? false : 1;
+        $scope.changesSaved = !editor.name ? false : 1;
         $scope.$watch('afform', function () {
           $scope.changesSaved = $scope.changesSaved === 1;
         }, true);
           .then(function (data) {
             $scope.saving = false;
             $scope.afform.name = data[0].name;
-            // FIXME: This causes an unnecessary reload when saving a new form
-            $location.search('name', data[0].name);
+            $location.url('/edit/' + data[0].name);
           });
       };
 
index acd32c00562e140f6d9a8f58244a2773bcf5bb7a..72e4ea3b7b26e85e19a5a918b2917c84672f8b5e 100644 (file)
@@ -1,4 +1,6 @@
-<div id="afGuiEditor" class="crm-flex-box">
-  <div id="afGuiEditor-palette" class="crm-flex-3" ng-include="'~/afGuiEditor/afGuiEditorPalette.html'"></div>
-  <div id="afGuiEditor-canvas" class="crm-flex-5" ng-include="'~/afGuiEditor/afGuiEditorCanvas.html'"></div>
+<div id="bootstrap-theme">
+  <div id="afGuiEditor" class="crm-flex-box">
+    <div id="afGuiEditor-palette" class="crm-flex-3" ng-include="'~/afGuiEditor/afGuiEditorPalette.html'"></div>
+    <div id="afGuiEditor-canvas" class="crm-flex-5" ng-include="'~/afGuiEditor/afGuiEditorCanvas.html'"></div>
+  </div>
 </div>
diff --git a/ext/afform/admin/templates/CRM/AfformAdmin/Page/Base.tpl b/ext/afform/admin/templates/CRM/AfformAdmin/Page/Base.tpl
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/ext/afform/admin/xml/Menu/afform_admin.xml b/ext/afform/admin/xml/Menu/afform_admin.xml
new file mode 100644 (file)
index 0000000..61400dc
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<menu>
+  <item>
+    <path>civicrm/admin/afform</path>
+    <page_callback>CRM_AfformAdmin_Page_Base</page_callback>
+    <access_arguments>administer CiviCRM</access_arguments>
+  </item>
+</menu>
index a69ee5c29207f9497607a4efa59c3e506e889376..ae2a328d0ea602f988c2b21d9df170b9801e85e2 100644 (file)
@@ -125,6 +125,9 @@ class Afform extends Generic\AbstractEntity {
         [
           'name' => 'name',
         ],
+        [
+          'name' => 'type',
+        ],
         [
           'name' => 'requires',
         ],
index dc0847fc135de740358d7b517322581513ad553e..c4eee679c6908382e065a01f6b5800c6414e77a4 100644 (file)
@@ -1,4 +1,5 @@
 {
   "title": "Household Name (default)",
+  "type": "block",
   "block": "Household"
 }
index 3d05402fb0885a9539830d9068a5fb39a0bc38a4..51c4596ea683f1cee3ba6bb36f7b6c4ad4797495 100644 (file)
@@ -1,4 +1,5 @@
 {
   "title": "Individual Name (default)",
+  "type": "block",
   "block": "Individual"
 }
index 34ab2fd53d6b31a19e972aacf417f5e39c944aa3..e3ac17c246f458ae91c12223c210d78f368d4599 100644 (file)
@@ -1,4 +1,5 @@
 {
   "title": "Organization Name (default)",
+  "type": "block",
   "block": "Organization"
 }
index 2a26888f4a1fb133a9b5fa39250f9882c3756942..27775770b667396dd401dacad5d5ad2e1c354ceb 100644 (file)
@@ -1,5 +1,6 @@
 {
   "title": "Address Block (default)",
+  "type": "block",
   "block": "Contact",
   "join": "Address",
   "repeat": true
index b09da6a075b7cd4055fe5af11dec24d4c40486a9..7c50c579dc094ee82db9ddc451df3adf2c7b4ae1 100644 (file)
@@ -1,5 +1,6 @@
 {
   "title": "Email (default)",
+  "type": "block",
   "block": "Contact",
   "join": "Email",
   "repeat": true
index 2ef6b577cba5c25fe11e0a3e34e514ac75c2a206..3ec912975b61bf0e8d3a6ccdced105247fa5b0c3 100644 (file)
@@ -1,5 +1,6 @@
 {
   "title": "IM (default)",
+  "type": "block",
   "block": "Contact",
   "join": "IM",
   "repeat": true
index c40af8dcb2ca8567b96dca37ed940221d083ffc1..821d2888408f22b5f3739cab9cc494e0624d1f50 100644 (file)
@@ -1,5 +1,6 @@
 {
   "title": "Phone (default)",
+  "type": "block",
   "block": "Contact",
   "join": "Phone",
   "repeat": true
index 4819bc92d84870e7e2b89c67da09fd34ac927d7f..b39dd9cd73177f5098acc183489e827d9eeb7378 100644 (file)
@@ -1,5 +1,6 @@
 {
   "title": "Website (default)",
+  "type": "block",
   "block": "Contact",
   "join": "Website",
   "repeat": true