Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions modules/crm_tools/decoupled_auth_crm_tools.info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: Simple CRM Tools
Comment thread
andrewbelcher marked this conversation as resolved.
description: "Tools for building CRM modules."
Comment thread
andrewbelcher marked this conversation as resolved.
core: 8.x
type: module
package: Tool
Comment thread
andrewbelcher marked this conversation as resolved.

dependencies:
- decoupled_auth
40 changes: 40 additions & 0 deletions modules/crm_tools/decoupled_auth_crm_tools.module
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* @file
* Simple implementation of Decoupled User Authentication Tools for CRM.
Comment thread
andrewbelcher marked this conversation as resolved.
*/

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\ContentEntityBase;

/**
* Implements hook_entity_presave().
*
* Update status log fields.
*/
function decoupled_auth_crm_tools_entity_presave(EntityInterface $entity) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this not live in the field \Drupal\Core\Field\FieldItemListInterface::preSave?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I tried all of the fielditemlist hooks... Unless the field actually is set pre save, nothing on the fielditemlist gets triggered.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContentEntityStorageBase::invokeFieldMethod is invoked for each field on presave, I'm surprised this doesn't work and is worth checking again.

if (!$entity instanceof ContentEntityBase) {
return;
}

// Check for status log fields.
foreach ($entity->getFieldDefinitions() as $field_name => $definition) {
/* @var \Drupal\Core\Field\FieldDefinitionInterface $definition */
if ($definition->getType() == 'status_log') {
$source_field = $definition->getFieldStorageDefinition()->getSetting('source_field');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's put all of this logic inside the status log field list class.


$value = $entity->{$source_field}->value;
$original_value = $entity->original->{$source_field}->value;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use \Drupal\Core\Field\FieldItemBase::mainPropertyName rather than assuming value?

if ($value !== $original_value) {
$values = [
'value' => $value,
'previous' => $original_value,
'uid' => \Drupal::currentUser()->id(),
'timestamp' => \Drupal::time()->getRequestTime(),
];
$entity->{$field_name}->appendItem($values);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Drupal\decoupled_auth_crm_tools\Plugin\Field\FieldFormatter;

use Drupal\Component\Datetime\DateTimePlus;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Defines the 'status_log_list' field formatter.
*
* @FieldFormatter(
* id = "status_log_list",
* label = @Translation("Status Log (list)"),
* field_types = {
* "status_log"
* }
* )
*/
class StatusLogFormatter extends FormatterBase implements ContainerFactoryPluginInterface {

/**
* {@inheritdoc}
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks these are being overridden with no changes?

return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['label'],
$configuration['view_mode'],
$configuration['third_party_settings']
);
}

/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
// @todo Add human readable labels.
Comment thread
andrewbelcher marked this conversation as resolved.
$elements = [];
foreach ($items as $delta => $item) {
/* @var \Drupal\decoupled_auth_crm_tools\Plugin\Field\FieldType\StatusLog $item */
$values = $item->getValue();
if (is_array($values) && !empty($values['value'])) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use $item->isEmpty()? And is that a scenario that's possible once you get to the formatter?


$elements[$delta]['value'] = [
'#type' => 'html_tag',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be better if $elements['#type'] = 'item_list'; and the values were the #items?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might mess with other things as I think $elements[$delta] is the expected pattern...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatter can say it handles multiple values and then deal with the whole lot as one.

'#tag' => 'p',
'#attributes' => ['class' => []],
'#value' => $this->t('@time: Status was changed from %status_old to %status_new by @username.', [
'%status_new' => $values['value'],
'%status_old' => $values['previous'],
'@time' => DateTimePlus::createFromTimestamp($values['timestamp'])->format('Y-m-d H:i:s'),
'@username' => User::load($values['uid'])->label(),
]),
];

}
}

return $elements;
}

}
168 changes: 168 additions & 0 deletions modules/crm_tools/src/Plugin/Field/FieldType/StatusLog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

namespace Drupal\decoupled_auth_crm_tools\Plugin\Field\FieldType;

use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;

/**
* Plugin implementation of the 'status_log' field type.
*
* @FieldType(
* id = "status_log",
* label = @Translation("Status log"),
* description = @Translation("Logs changes to a status"),
* default_widget = "string_textfield",
* default_formatter = "status_log_list"
* )
*/
class StatusLog extends FieldItemBase {

/**
* {@inheritdoc}
*/
public static function defaultStorageSettings() {
return [
'max_length' => 255,
] + parent::defaultStorageSettings();
}

/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
// Prevent early t() calls by using the TranslatableMarkup.
$properties['value'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Status'))
->setRequired(TRUE);

$properties['previous'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Previous Status'));

$properties['timestamp'] = DataDefinition::create('timestamp')
->setLabel(new TranslatableMarkup('Changed timestamp'))
->setRequired(TRUE);

$properties['uid'] = DataDefinition::create('integer')
->setLabel(new TranslatableMarkup('User responsible for change'));

return $properties;
}

/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
],
'previous' => [
'type' => 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
],
'timestamp' => [
'type' => 'int',
],
'uid' => [
'type' => 'int',
],
],
'indexes' => [
'value' => ['value'],
'previous' => ['previous'],
],
];
}

/**
* {@inheritdoc}
*/
public function getConstraints() {
$constraints = parent::getConstraints();

if ($max_length = $this->getSetting('max_length')) {
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints[] = $constraint_manager->create('ComplexData', [
'value' => [
'Length' => [
'max' => $max_length,
'maxMessage' => t('%name: may not be longer than @max characters.', [
'%name' => $this->getFieldDefinition()->getLabel(),
'@max' => $max_length
]),
],
],
]);
}

return $constraints;
}

/**
* {@inheritdoc}
*/
public function applyDefaultValue($notify = TRUE) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant.

parent::applyDefaultValue($notify);
return $this;
}

/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
$random = new Random();

$values['value'] = $random->word(mt_rand(1, $field_definition->getSetting('max_length')));
$values['previous'] = $random->word(mt_rand(1, $field_definition->getSetting('max_length')));
$values['uid'] = mt_rand(0, 1);
$values['timestamp'] = mt_rand(1262055681, 1262055681);

return $values;
}

/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
$elements = [];

$elements['max_length'] = [
'#type' => 'number',
'#title' => t('Maximum length'),
'#default_value' => $this->getSetting('max_length'),
'#required' => TRUE,
'#description' => t('The maximum length of the field in characters.'),
'#min' => 1,
'#disabled' => $has_data,
];

// @todo Make this field a select field of existing fields on entity.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we do this, we need to filter the fields by appropriate ones. My suggestion would be ones where there is a main value and the main value is a string... We may want to put some similar validation somewhere else for programmatic fields, but not exactly sure where that should live...

$elements['source_field'] = [
'#type' => 'textfield',
'#title' => t('Source Field'),
'#default_value' => $this->getSetting('source_field'),
'#required' => TRUE,
'#description' => t('The field that stores the status being logged.'),
'#disabled' => $has_data,
];

return $elements;
}

/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}

}