Afform Gui - Support editing blocks
authorColeman Watts <coleman@civicrm.org>
Wed, 27 Jan 2021 19:24:01 +0000 (14:24 -0500)
committerColeman Watts <coleman@civicrm.org>
Sat, 30 Jan 2021 01:41:06 +0000 (20:41 -0500)
ext/afform/admin/ang/afAdmin/afAdminList.controller.js
ext/afform/admin/ang/afAdmin/afAdminList.html
ext/afform/admin/ang/afGuiEditor.css
ext/afform/admin/ang/afGuiEditor.js
ext/afform/admin/ang/afGuiEditor/afGuiEditor.component.js
ext/afform/admin/ang/afGuiEditor/afGuiEditorCanvas.html
ext/afform/admin/ang/afGuiEditor/afGuiEditorPalette.html
ext/afform/admin/ang/afGuiEditor/afGuiEntity.component.js
ext/afform/admin/ang/afGuiEditor/afGuiEntity.html
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.component.js
ext/afform/admin/ang/afGuiEditor/elements/afGuiContainer.html

index 682b17aa3321b9c46bcad033635fad3c228fbdbd..10f4e7fc9d43835fa16999144a2d1c91fa6066cb 100644 (file)
@@ -8,11 +8,20 @@
     $scope.crmUrl = CRM.url;
 
     this.tabs = CRM.afAdmin.afform_type;
+    $scope.tabs = _.indexBy(ctrl.tabs, 'name');
+    _.each(['form', 'block', 'search'], function(type) {
+      if ($scope.tabs[type]) {
+        $scope.tabs[type].options = [];
+        if (type === 'form') {
+          $scope.tabs.form.default = '#create/form/Individual';
+        }
+      }
+    });
 
     this.afforms = _.transform(afforms, function(afforms, afform) {
-      var type = afform.type || 'system';
-      afforms[type] = afforms[type] || [];
-      afforms[type].push(afform);
+      afform.type = afform.type || 'system';
+      afforms[afform.type] = afforms[afform.type] || [];
+      afforms[afform.type].push(afform);
     }, {});
 
     $scope.$bindToRoute({
       default: ctrl.tabs[0].name
     });
 
+    this.createLinks = function() {
+      ctrl.searchCreateLinks = '';
+      if ($scope.tabs[ctrl.tab].options.length) {
+        return;
+      }
+      var links = [];
+
+      if (ctrl.tab === 'form') {
+        _.each(CRM.afGuiEditor.entities, function(entity, name) {
+          if (entity.defaults) {
+            links.push({
+              url: '#create/form/' + name,
+              label: entity.label,
+              icon: entity.icon
+            });
+          }
+        });
+        $scope.tabs.form.options = _.sortBy(links, 'Label');
+      }
+
+      if (ctrl.tab === 'block') {
+        _.each(CRM.afGuiEditor.entities, function(entity, name) {
+          if (entity.defaults) {
+            links.push({
+              url: '#create/block/' + name,
+              label: entity.label,
+              icon: entity.icon
+            });
+          }
+        });
+        $scope.tabs.block.options = _.sortBy(links, 'Label');
+      }
+    };
+
     this.revert = function(afform) {
       var index = _.findIndex(ctrl.afforms[ctrl.tab], {name: afform.name});
       if (index > -1) {
index 80af2d876c4ddc22b2949c013e17e2a8ea6e242f..5ab6d0cc127fdd0cd606371cbc77f68d2cb2557a 100644 (file)
@@ -1,21 +1,36 @@
 <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>
+      <a href ng-click="$ctrl.tab = tab.name; $ctrl.searchAfformList = ''"><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') }}">
+    <label for="afform-list-filter">{{:: ts('Filter:') }}</label>
+    <input class="form-control" type="search" id="afform-list-filter" ng-model="$ctrl.searchAfformList" placeholder="&#xf002">
+    <div class="btn-group pull-right" ng-if="tabs[$ctrl.tab].options">
+      <a ng-if="tabs[$ctrl.tab].default" href="{{ tabs[$ctrl.tab].default }}" class="btn btn-primary">
+        {{ ts('New %1', {1: tabs[$ctrl.tab].label }) }}
+      </a>
+      <button ng-click="$ctrl.createLinks()" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+        <span ng-class="{'sr-only': tabs[$ctrl.tab].default}">{{ ts('New %1', {1: tabs[$ctrl.tab].label }) }}</span>
+        <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li>
+          <input ng-if="tabs[$ctrl.tab].options.length" type="search" class="form-control" placeholder="&#xf002" ng-model="$ctrl.searchCreateLinks">
+          <a href ng-if="!tabs[$ctrl.tab].options.length"><i class="crm-i fa-spinner fa-spin"></i></a>
+        </li>
+        <li ng-repeat="link in tabs[$ctrl.tab].options | filter:$ctrl.searchCreateLinks">
+          <a href="{{ link.url }}">
+            <i class="crm-i {{ link.icon }}"></i>
+            {{ link.label }}
+          </a>
+        </li>
+      </ul>
+    </div>
   </div>
 
   <table>
@@ -29,7 +44,7 @@
     </tr>
     </thead>
     <tbody>
-    <tr ng-repeat="afform in $ctrl.afforms[$ctrl.tab] | filter:$ctrl.search">
+    <tr ng-repeat="afform in $ctrl.afforms[$ctrl.tab] | filter:$ctrl.searchAfformList">
       <td>{{afform.title}}</td>
       <td>
         <code>{{afform.name}}</code>
@@ -41,7 +56,7 @@
       </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 ng-if="afform.type !== 'system'" 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>
index ccacca1d671ecfbaa33ed9c385d67dc27bd1c848..55703580bf07d8336880decf3af2886d0f473755 100644 (file)
   font-size: 12px;
 }
 
-#afGuiEditor input[type=search]::placeholder {
+#bootstrap-theme input[type=search]::placeholder {
   font-family: FontAwesome;
   text-align: right;
 }
-#afGuiEditor input[type=search]:-ms-input-placeholder {
+#bootstrap-theme input[type=search]:-ms-input-placeholder {
   font-family: FontAwesome;
   text-align: right;
 }
-#afGuiEditor input[type=search]::-ms-input-placeholder {
+#bootstrap-theme input[type=search]::-ms-input-placeholder {
   font-family: FontAwesome;
   text-align: right;
 }
index 5b1253fc920f8e61bc2155171f65ac1257cf67ec..ca6947e87f545ee31679fc8fd42c84dcae6fddab 100644 (file)
 
         meta: CRM.afGuiEditor,
 
-        getField: function(entityType, fieldName) {
-          return CRM.afGuiEditor.entities[entityType].fields[fieldName];
+        getEntity: function(entityName) {
+          return CRM.afGuiEditor.entities[entityName];
+        },
+
+        getField: function(entityName, fieldName) {
+          return CRM.afGuiEditor.entities[entityName].fields[fieldName];
         },
 
         // Recursively searches a collection and its children using _.filter
index 68fa65a9c6f91bade3d317e88fb53963f598f8ff..80ce49a798bcb00e372c687b9cda8ca853cee0ad 100644 (file)
         }
         $scope.canvasTab = 'layout';
         $scope.layoutHtml = '';
-        editor.layout = afGui.findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0];
-        $scope.entities = afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name');
+        editor.layout = {'#children': []};
+        $scope.entities = {};
 
-        if (editor.mode === 'create') {
-          editor.addEntity(editor.entity);
-          editor.layout['#children'].push(afGui.meta.elements.submit.element);
+        if ($scope.afform.type === 'form') {
+          editor.allowEntityConfig = true;
+          editor.layout['#children'] = afGui.findRecursive($scope.afform.layout, {'#tag': 'af-form'})[0]['#children'];
+          $scope.entities = afGui.findRecursive(editor.layout['#children'], {'#tag': 'af-entity'}, 'name');
+
+          if (editor.mode === 'create') {
+            editor.addEntity(editor.entity);
+            editor.layout['#children'].push(afGui.meta.elements.submit.element);
+          }
+        }
+
+        else if ($scope.afform.type === 'block') {
+          editor.layout['#children'] = $scope.afform.layout;
+          editor.blockEntity = $scope.afform.join || $scope.afform.block;
+          $scope.entities[editor.blockEntity] = {
+            type: editor.blockEntity,
+            name: editor.blockEntity,
+            label: afGui.getEntity(editor.blockEntity).label
+          };
         }
 
         // Set changesSaved to true on initial load, false thereafter whenever changes are made to the model
index 53365daad9eb8cfa333578d7d64944b5ef5c21f7..497c219ace88c582ad8d3673fb193bba47de9dda 100644 (file)
@@ -25,7 +25,7 @@
 
   </div>
   <div id="afGuiEditor-canvas-body" class="panel-body" ng-if="canvasTab === 'layout'">
-    <af-gui-container ng-if="editor.layout" node="editor.layout" entity-name="" ></af-gui-container>
+    <af-gui-container ng-if="editor.layout" node="editor.layout" entity-name="editor.blockEntity" data-entity="{{ editor.blockEntity }}" ></af-gui-container>
   </div>
   <div class="panel-body" ng-if="canvasTab === 'markup'">
     <p class="help-block">{{:: ts('This is a read-only preview of the auto-generated markup.') }}</p>
index 9307e904c7582658ab3beb6bdfb845fdd9bb557f..f166d96ed1675f08b5a1e55a07ed645d8ed67095 100644 (file)
@@ -7,12 +7,13 @@
       </li>
       <li role="presentation" ng-repeat="entity in entities" ng-class="{active: selectedEntityName === entity.name}">
         <a href ng-click="editor.selectEntity(entity.name)">
-          <span ng-if="!entity.loading" af-gui-editable ng-model="entity.label">{{ entity.label }}</span>
+          <span ng-if="!entity.loading && editor.allowEntityConfig" af-gui-editable ng-model="entity.label">{{ entity.label }}</span>
+          <span ng-if="!entity.loading && !editor.allowEntityConfig">{{ entity.label }}</span>
           <i ng-if="entity.loading" class="crm-i fa-spin fa-spinner"></i>
         </a>
       </li>
-      <li role="presentation" class="dropdown">
-        <a href class="dropdown-toggle" data-toggle="dropdown">
+      <li role="presentation" class="dropdown" ng-if="editor.allowEntityConfig">
+        <a href class="dropdown-toggle" data-toggle="dropdown" title="{{ ts('Add Entity') }}">
           <span><i class="crm-i fa-plus"></i></span>
         </a>
         <ul class="dropdown-menu">
index 4c5c8d8a470c79cacb395d8507d42a4e8dd9cf47..3de79cf717b6e1bb6831dbbf6b0df5c77b069bd6 100644 (file)
           if (!search || _.contains(name, search) || _.contains(element.title.toLowerCase(), search)) {
             var node = _.cloneDeep(element.element);
             if (name === 'fieldset') {
+              if (!ctrl.editor.allowEntityConfig) {
+                return;
+              }
               node['af-fieldset'] = ctrl.entity.name;
             }
             $scope.elementList.push(node);
index f55fc71a8f56a04103757748d26249ea5bb0487d..7b67938b3cbb5225903471b2b1e1c08e33d8849d 100644 (file)
   </fieldset>
 </div>
 
-<a href ng-click="$ctrl.editor.removeEntity($ctrl.entity.name)" class="btn btn-sm btn-danger-outline af-gui-remove-entity" title="{{ ts('Remove %1', {1: getMeta().label}) }}">
+<a ng-if="!$ctrl.entity.loading && $ctrl.editor.allowEntityConfig" href ng-click="$ctrl.editor.removeEntity($ctrl.entity.name)" class="btn btn-sm btn-danger-outline af-gui-remove-entity" title="{{ ts('Remove %1', {1: getMeta().label}) }}">
   <i class="crm-i fa-trash"></i>
 </a>
 
-<fieldset ng-if="!$ctrl.entity.loading">
+<fieldset ng-if="!$ctrl.entity.loading && $ctrl.editor.allowEntityConfig">
   <legend>{{:: ts('Options') }}</legend>
   <div ng-include="'~/afGuiEditor/entityConfig/' + $ctrl.entity.type + '.html'"></div>
 </fieldset>
index 0953d61916313b0c51bf484545713b145847e511..5ebe110b48fa1bcd9eaacc0f560a4d1c1a54243f 100644 (file)
@@ -16,7 +16,7 @@
         ctrl = this;
 
       this.$onInit = function() {
-        if ((ctrl.node['#tag'] in afGui.meta.blocks) || ctrl.join) {
+        if (ctrl.node['#tag'] && ((ctrl.node['#tag'] in afGui.meta.blocks) || ctrl.join)) {
           var blockNode = getBlockNode(),
             blockTag = blockNode ? blockNode['#tag'] : null;
           if (blockTag && (blockTag in afGui.meta.blocks) && !afGui.meta.blocks[blockTag].layout) {
       this.node = ctrl.node;
 
       this.getNodeType = function(node) {
-        if (!node) {
+        if (!node || !node['#tag']) {
           return null;
         }
         if (node['#tag'] === 'af-field') {
index a1ae54be3e2aa1ff4697aa770af251afb599bd87..bf9057694227ae55009bcebd114532af8de47798 100644 (file)
@@ -1,4 +1,4 @@
-<div class="af-gui-bar" ng-if="$ctrl.node['#tag'] !== 'af-form'" ng-click="selectEntity()" >
+<div class="af-gui-bar" ng-if="$ctrl.node['#tag']" ng-click="selectEntity()" >
   <div ng-if="!$ctrl.loading" class="form-inline" af-gui-menu>
     <span ng-if="$ctrl.getNodeType($ctrl.node) == 'fieldset'">{{ $ctrl.editor.getEntity($ctrl.entityName).label }}</span>
     <span ng-if="block">{{ $ctrl.join ? ts($ctrl.join) + ':' : ts('Block:') }}</span>