From 51864a6308200512dcc3f01c8e4ef2b1b7691943 Mon Sep 17 00:00:00 2001 From: David Craig Date: Fri, 24 Jul 2015 14:37:48 +1200 Subject: [PATCH] API new form editor --- .travis.yml | 5 +- _config.php | 2 + _config/userforms.yml | 9 - .../UserFormFieldEditorExtension.php | 186 +++ code/formfields/FieldEditor.php | 338 ----- .../formfields/UserformsTreeDropdownField.php | 11 +- code/forms/UserForm.php | 158 +++ code/model/EditableCustomRule.php | 60 + code/model/UserDefinedForm.php | 1171 +++-------------- code/model/formfields/EditableCheckbox.php | 40 +- .../formfields/EditableCheckboxGroupField.php | 12 +- .../EditableCountryDropdownField.php | 13 +- code/model/formfields/EditableDateField.php | 43 +- code/model/formfields/EditableDropdown.php | 18 +- code/model/formfields/EditableEmailField.php | 2 + code/model/formfields/EditableFileField.php | 53 +- code/model/formfields/EditableFormField.php | 523 ++++---- code/model/formfields/EditableFormHeading.php | 60 +- .../model/formfields/EditableLiteralField.php | 73 +- .../formfields/EditableMemberListField.php | 47 +- .../EditableMultipleOptionField.php | 80 +- .../model/formfields/EditableNumericField.php | 36 +- code/model/formfields/EditableOption.php | 57 +- code/model/formfields/EditableRadioField.php | 28 +- code/model/formfields/EditableTextField.php | 94 +- .../UserDefinedForm_EmailRecipient.php | 378 ++++++ ...serDefinedForm_EmailRecipientCondition.php | 54 + .../recipients/UserFormRecipientEmail.php | 30 + .../UserFormRecipientItemRequest.php | 51 + code/model/submissions/SubmittedFileField.php | 6 +- code/tasks/UserFormsUpgradeService.php | 218 +++ code/tasks/UserFormsUpgradeTask.php | 22 + css/FieldEditor.css | 245 ---- docs/en/_images/add-fields.png | Bin 51745 -> 77696 bytes docs/en/_images/mergefield.png | Bin 43480 -> 46334 bytes docs/en/_images/mergefieldcontent.png | Bin 90415 -> 84314 bytes docs/en/user-documentation.md | 4 +- javascript/Recipient.js | 17 +- javascript/UserForm.js | 353 ----- templates/EditableFormField.ss | 134 -- templates/EditableOption.ss | 11 - templates/FieldEditor.ss | 31 - templates/Includes/CustomRule.ss | 28 - tests/EditableFormFieldTest.php | 265 +--- tests/EditableFormFieldTest.yml | 58 +- tests/FieldEditorTest.php | 46 - tests/UserDefinedFormControllerTest.php | 31 +- tests/UserDefinedFormTest.php | 2 +- tests/UserDefinedFormTest.yml | 5 - tests/UserFormsUpgradeServiceTest.php | 216 +++ tests/UserFormsUpgradeServiceTest.yml | 57 + 51 files changed, 2367 insertions(+), 3014 deletions(-) delete mode 100644 _config/userforms.yml create mode 100644 code/extensions/UserFormFieldEditorExtension.php delete mode 100755 code/formfields/FieldEditor.php create mode 100644 code/forms/UserForm.php create mode 100644 code/model/EditableCustomRule.php create mode 100644 code/model/recipients/UserDefinedForm_EmailRecipient.php create mode 100644 code/model/recipients/UserDefinedForm_EmailRecipientCondition.php create mode 100644 code/model/recipients/UserFormRecipientEmail.php create mode 100644 code/model/recipients/UserFormRecipientItemRequest.php create mode 100644 code/tasks/UserFormsUpgradeService.php create mode 100644 code/tasks/UserFormsUpgradeTask.php delete mode 100755 css/FieldEditor.css delete mode 100644 javascript/UserForm.js delete mode 100755 templates/EditableFormField.ss delete mode 100644 templates/EditableOption.ss delete mode 100755 templates/FieldEditor.ss delete mode 100644 templates/Includes/CustomRule.ss delete mode 100644 tests/FieldEditorTest.php create mode 100644 tests/UserFormsUpgradeServiceTest.php create mode 100644 tests/UserFormsUpgradeServiceTest.yml diff --git a/.travis.yml b/.travis.yml index 640a63d..9acf8e4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ # See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details -language: php +language: php + +sudo: false + php: - 5.4 diff --git a/_config.php b/_config.php index f6092e9..10cdf25 100644 --- a/_config.php +++ b/_config.php @@ -3,3 +3,5 @@ if(!defined('USERFORMS_DIR')) { define('USERFORMS_DIR', basename(__DIR__)); } + +Deprecation::notification_version('3.0', 'userforms'); diff --git a/_config/userforms.yml b/_config/userforms.yml deleted file mode 100644 index 7445bea..0000000 --- a/_config/userforms.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: userforms ---- -LeftAndMain: - extra_requirements_javascript: - - userforms/javascript/UserForm.js - - extra_requirements_css: - - userforms/css/FieldEditor.css diff --git a/code/extensions/UserFormFieldEditorExtension.php b/code/extensions/UserFormFieldEditorExtension.php new file mode 100644 index 0000000..08efb2a --- /dev/null +++ b/code/extensions/UserFormFieldEditorExtension.php @@ -0,0 +1,186 @@ + 'EditableFormField' + ); + + /** + * Adds the field editor to the page. + * + * @return FieldList + */ + public function updateCMSFields(FieldList $fields) { + $fieldEditor = $this->getFieldEditorGrid(); + + $fields->insertAfter(new Tab('FormFields', _t('UserFormFieldEditorExtension.FORMFIELDS', 'Form Fields')), 'Main'); + $fields->addFieldToTab('Root.FormFields', $fieldEditor); + + return $fields; + } + + /** + * Gets the field editor, for adding and removing EditableFormFields. + * + * @return GridField + */ + public function getFieldEditorGrid() { + $fields = $this->owner->Fields(); + + $editableColumns = new GridFieldEditableColumns(); + $editableColumns->setDisplayFields(array( + 'ClassName' => function($record, $column, $grid) { + return DropdownField::create($column, '', $this->getEditableFieldClasses()); + }, + 'Title' => function($record, $column, $grid) { + return TextField::create($column, ' ') + ->setAttribute('placeholder', _t('UserDefinedForm.TITLE', 'Title')); + } + )); + + $fieldEditor = GridField::create( + 'Fields', + _t('UserDefinedForm.FIELDS', 'Fields'), + $fields, + GridFieldConfig::create() + ->addComponents( + $editableColumns, + new GridFieldButtonRow(), + new GridFieldAddNewInlineButton(), + new GridFieldEditButton(), + new GridFieldDeleteAction(), + new GridFieldToolbarHeader(), + new GridFieldOrderableRows('Sort'), + new GridState_Component(), + new GridFieldDetailForm() + ) + ); + + return $fieldEditor; + } + + /** + * @return array + */ + public function getEditableFieldClasses() { + $classes = ClassInfo::getValidSubClasses('EditableFormField'); + + // Remove classes we don't want to display in the dropdown. + $classes = array_diff($classes, array( + 'EditableFormField', + 'EditableMultipleOptionField' + )); + + $editableFieldClasses = array(); + + foreach ($classes as $key => $className) { + $singleton = singleton($className); + + if(!$singleton->canCreate()) { + continue; + } + + $editableFieldClasses[$className] = $singleton->i18n_singular_name(); + } + + return $editableFieldClasses; + } + + /** + * @see SiteTree::doPublish + * @param Page $original + * + * @return void + */ + public function onAfterPublish($original) { + // Remove fields on the live table which could have been orphaned. + $live = Versioned::get_by_stage("EditableFormField", "Live") + ->filter('ParentID', $original->ID); + + if($live) { + foreach($live as $field) { + $field->doDeleteFromStage('Live'); + } + } + + foreach($this->owner->Fields() as $field) { + $field->doPublish('Stage', 'Live'); + } + } + + /** + * @see SiteTree::doUnpublish + * @param Page $page + * + * @return void + */ + public function onAfterUnpublish($page) { + foreach($page->Fields() as $field) { + $field->doDeleteFromStage('Live'); + } + } + + /** + * @see SiteTree::duplicate + * @param DataObject $newPage + * + * @return DataObject + */ + public function onAfterDuplicate($newPage) { + foreach($this->owner->Fields() as $field) { + $newField = $field->duplicate(false); + $newField->ParentID = $newPage->ID; + $newField->ParentClass = $newPage->ClassName; + $newField->Version = 0; + $newField->write(); + + foreach ($field->DisplayRules() as $customRule) { + $newRule = $customRule->duplicate(false); + $newRule->ParentID = $newField->ID; + $newRule->Version = 0; + $newRule->write(); + } + } + + return $newPage; + } + + /** + * @see SiteTree::getIsModifiedOnStage + * @param boolean $isModified + * + * @return boolean + */ + public function getIsModifiedOnStage($isModified) { + if(!$isModified) { + foreach($this->owner->Fields() as $field) { + if($field->getIsModifiedOnStage()) { + $isModified = true; + break; + } + } + } + + return $isModified; + } + + /** + * @see SiteTree::doRevertToLive + * @param Page $page + * + * @return void + */ + public function onAfterRevertToLive($page) { + foreach($page->Fields() as $field) { + $field->publish('Live', 'Stage', false); + $field->writeWithoutVersion(); + } + } +} diff --git a/code/formfields/FieldEditor.php b/code/formfields/FieldEditor.php deleted file mode 100755 index d0dbefb..0000000 --- a/code/formfields/FieldEditor.php +++ /dev/null @@ -1,338 +0,0 @@ - '$Action' - ); - - private static $allowed_actions = array( - 'addfield', - 'addoptionfield', - 'handleField' - ); - - /** - * @param array $properties - * - * @return HTML - */ - public function FieldHolder($properties = array()) { - $add = Controller::join_links($this->Link('addfield')); - - $this->setAttribute('data-add-url', '\''. $add.'\''); - - return $this->renderWith("FieldEditor"); - } - - /** - * Returns whether a user can edit the form. - * - * @param Member $member - * - * @return boolean - */ - public function canEdit($member = null) { - if($this->readonly) { - return false; - } - - return $this->form->getRecord()->canEdit(); - } - - /** - * Returns whether a user delete a field in the form. The - * {@link EditableFormField} instances check if they can delete themselves - * but this counts as an {@link self::canEdit()} function rather than a - * delete. - * - * @param Member $member - * @return boolean - */ - public function canDelete($member = null) { - if($this->readonly) return false; - - return $this->form->getRecord()->canEdit(); - } - - /** - * Transform this form field to a readonly version. - * - * @return ViewableData_Customised - */ - public function performReadonlyTransformation() { - $clone = clone $this; - $clone->readonly = true; - $fields = $clone->Fields(); - - if($fields) { - foreach($fields as $field) { - $field->setReadonly(); - } - } - - return $clone->customise(array( - 'Fields' => $fields - )); - } - - /** - * Return the fields. - * - * @return RelationList - */ - public function Fields() { - if($this->form && $this->form->getRecord() && $this->name) { - $relationName = $this->name; - $fields = $this->form->getRecord()->getComponents($relationName); - - if($fields) { - foreach($fields as $field) { - if(!$this->canEdit() && is_a($field, 'FormField')) { - $fields->remove($field); - $fields->push($field->performReadonlyTransformation()); - } - } - } - - return $fields; - } - } - - /** - * Return a {@link ArrayList} of all the addable fields to populate the add - * field menu. - * - * @return ArrayList - */ - public function CreatableFields() { - $fields = ClassInfo::subclassesFor('EditableFormField'); - - if($fields) { - array_shift($fields); // get rid of subclass 0 - asort($fields); // get in order - - $output = new ArrayList(); - - foreach($fields as $field => $title) { - // get the nice title and strip out field - $niceTitle = _t( - $field.'.SINGULARNAME', - $title - ); - - if($niceTitle && $field != "EditableMultipleOptionField") { - $output->push(new ArrayData(array( - 'ClassName' => $field, - 'Title' => "$niceTitle" - ))); - } - } - - return $output; - } - - return false; - } - - /** - * Handles saving the page. Needs to keep an eye on fields and options which - * have been removed / added - * - * @param DataObject $record - */ - public function saveInto(DataObjectInterface $record) { - $name = $this->name; - $fieldSet = $record->$name(); - - // store the field IDs and delete the missing fields - // alternatively, we could delete all the fields and re add them - $missingFields = array(); - - foreach($fieldSet as $existingField) { - $missingFields[$existingField->ID] = $existingField; - } - - if(isset($_REQUEST[$name]) && is_array($_REQUEST[$name])) { - foreach($_REQUEST[$name] as $newEditableID => $newEditableData) { - if(!is_numeric($newEditableID)) continue; - - // get it from the db - $editable = DataObject::get_by_id('EditableFormField', $newEditableID); - - // if it exists in the db update it - if($editable) { - - // remove it from the removed fields list - if(isset($missingFields[$editable->ID]) && isset($newEditableData) && is_array($newEditableData)) { - unset($missingFields[$editable->ID]); - } - - // set form id - if($editable->ParentID == 0) { - $editable->ParentID = $record->ID; - } - - // save data - $editable->populateFromPostData($newEditableData); - } - } - } - - // remove the fields not saved - if($this->canEdit()) { - foreach($missingFields as $removedField) { - if(is_numeric($removedField->ID)) { - // check we can edit this - $removedField->delete(); - } - } - } - } - - /** - * Add a field to the field editor. Called via a ajax get. - * - * @return bool|html - */ - public function addfield() { - if(!SecurityToken::inst()->checkRequest($this->request)) { - return $this->httpError(400); - } - - // get the last field in this form editor - $parentID = $this->form->getRecord()->ID; - - if($parentID) { - $parentID = (int)$parentID; - - $sqlQuery = new SQLQuery(); - $sqlQuery = $sqlQuery - ->setSelect("MAX(\"Sort\")") - ->setFrom("\"EditableFormField\"") - ->setWhere("\"ParentID\" = $parentID"); - - $sort = $sqlQuery->execute()->value() + 1; - - $className = (isset($_REQUEST['Type'])) ? $_REQUEST['Type'] : ''; - - if(!$className) { - // A possible reason for the classname being blank is because we have execeded - // the number of input requests - // http://www.php.net/manual/en/info.configuration.php#ini.max-input-vars - $maxRequests = ini_get('max_input_vars'); - $numRequests = count($_REQUEST, COUNT_RECURSIVE); - if ($numRequests > $maxRequests) { - $error = sprintf('You have exceded the maximum number %s of input requests', - "[$maxRequests]"); - user_error($error, E_USER_WARNING); - } - user_error('Please select a field type to created', E_USER_WARNING); - } - - if(is_subclass_of($className, "EditableFormField")) { - $field = new $className(); - $field->ParentID = $this->form->getRecord()->ID; - $field->Name = $field->class . $field->ID; - $field->Sort = $sort; - $field->write(); - - return $field->EditSegment(); - } - } - - return false; - } - - /** - * Return the html for a field option such as a - * dropdown field or a radio check box field - * - * @return bool|html - */ - public function addoptionfield() { - if(!SecurityToken::inst()->checkRequest($this->request)) { - return $this->httpError(400); - } - - // passed via the ajax - $parent = (isset($_REQUEST['Parent'])) ? $_REQUEST['Parent'] : false; - - // work out the sort by getting the sort of the last field in the form +1 - if($parent) { - $sql_parent = (int)$parent; - - $parentObj = EditableFormField::get()->byID($parent); - $optionClass = ($parentObj && $parentObj->exists()) ? $parentObj->getRelationClass('Options') : 'EditableOption'; - - $sqlQuery = new SQLQuery(); - $sqlQuery = $sqlQuery - ->setSelect("MAX(\"Sort\")") - ->setFrom("\"EditableOption\"") - ->setWhere("\"ParentID\" = $sql_parent"); - - $sort = $sqlQuery->execute()->value() + 1; - - if($parent) { - $object = Injector::inst()->create($optionClass); - $object->write(); - $object->ParentID = $parent; - $object->Sort = $sort; - $object->Name = 'option' . $object->ID; - $object->write(); - - return $object->EditSegment(); - } - } - - return false; - } - - /** - * Pass sub {@link FormField} requests through the editor. For example, - * option fields need to be able to call themselves. - * - * @param SS_HTTPRequest - */ - public function handleField(SS_HTTPRequest $request) { - if(!SecurityToken::inst()->checkRequest($this->request)) { - return $this->httpError(400); - } - - $fields = $this->Fields(); - - // extract the ID and option field name - preg_match( - '/Fields\[(?P\d+)\]\[CustomSettings\]\[(?P