diff --git a/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql
index ceef7055ca38e..dc847730c5a74 100644
--- a/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql
+++ b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-05-15.sql
@@ -127,7 +127,8 @@ INSERT INTO `#__workflow_transitions` (`id`, `asset_id`, `published`, `ordering`
INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES
(0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 0, '', '{}', 0, NULL, 0, 0),
-(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, '', '{}', 0, NULL, 0, 0);
+(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, '', '{}', 0, NULL, 0, 0),
+(0, 'plg_workflow_featuring', 'plugin', 'featuring', 'workflow', 0, 1, 1, 0, '', '{}', 0, NULL, 0, 0);
--
-- Creating Associations for existing content
diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql
index 107b73c480fb2..e873e2c2c32c3 100644
--- a/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql
+++ b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-05-15.sql
@@ -123,7 +123,8 @@ INSERT INTO "#__workflow_transitions" ("id", "asset_id", "published", "ordering"
INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "checked_out", "checked_out_time", "ordering", "state") VALUES
(0, 'com_workflow', 'component', 'com_workflow', '', 1, 1, 0, 0, '', '{}', 0, '1970-01-01 00:00:00', 0, 0),
-(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, '', '{}', 0, NULL, 0, 0);
+(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, '', '{}', 0, NULL, 0, 0),
+(0, 'plg_workflow_featuring', 'plugin', 'featuring', 'workflow', 0, 1, 1, 0, '', '{}', 0, NULL, 0, 0);
--
-- Creating Associations for existing content
diff --git a/administrator/components/com_content/src/Extension/ContentComponent.php b/administrator/components/com_content/src/Extension/ContentComponent.php
index 06265cd471be8..01ad75352eb1c 100644
--- a/administrator/components/com_content/src/Extension/ContentComponent.php
+++ b/administrator/components/com_content/src/Extension/ContentComponent.php
@@ -53,6 +53,14 @@ class ContentComponent extends MVCComponent implements
CategoryServiceTrait::getStateColumnForSection insteadof TagServiceTrait;
}
+ /** @var array Supported functionality */
+ protected $supportedFunctionality = [
+ 'core.featured' => [
+ 'com_content.articles'
+ ],
+ 'joomla.state' => true,
+ ];
+
/**
* The trashed condition
*
diff --git a/administrator/components/com_content/src/Model/ArticleModel.php b/administrator/components/com_content/src/Model/ArticleModel.php
index 47af27fced737..bba8b0e34cead 100644
--- a/administrator/components/com_content/src/Model/ArticleModel.php
+++ b/administrator/components/com_content/src/Model/ArticleModel.php
@@ -73,10 +73,49 @@ class ArticleModel extends AdminModel implements WorkflowModelInterface
*/
protected $associationsContext = 'com_content.item';
+ /**
+ * The event to trigger before changing featured status one or more items.
+ *
+ * @var string
+ * @since 4.0
+ */
+ protected $event_before_change_featured = null;
+
+ /**
+ * The event to trigger after changing featured status one or more items.
+ *
+ * @var string
+ * @since 4.0
+ */
+ protected $event_after_change_featured = null;
+
+ /**
+ * Constructor.
+ *
+ * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request).
+ * @param MVCFactoryInterface $factory The factory.
+ * @param FormFactoryInterface $formFactory The form factory.
+ *
+ * @since 1.6
+ * @throws \Exception
+ */
public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null)
{
+ $config['events_map'] = $config['events_map'] ?? [];
+
+ $config['events_map'] = array_merge(
+ ['featured' => 'content'],
+ $config['events_map']
+ );
+
parent::__construct($config, $factory, $formFactory);
+ // Set the featured status change events
+ $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured;
+ $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured';
+ $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured;
+ $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured';
+
$this->setUpWorkflow('com_content.article');
}
@@ -825,9 +864,15 @@ public function save($data)
public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null)
{
// Sanitize the ids.
- $pks = (array) $pks;
- $pks = ArrayHelper::toInteger($pks);
- $value = (int) $value;
+ $pks = (array) $pks;
+ $pks = ArrayHelper::toInteger($pks);
+ $value = (int) $value;
+ $context = $this->option . '.' . $this->name;
+
+ $this->workflowBeforeStageChange();
+
+ // Include the plugins for the change of state event.
+ PluginHelper::importPlugin($this->events_map['featured']);
// Convert empty strings to null for the query.
if ($featuredUp === '')
@@ -849,6 +894,16 @@ public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = n
$table = $this->getTable('Featured', 'Administrator');
+ // Trigger the before change state event.
+ $result = Factory::getApplication()->triggerEvent($this->event_before_change_featured, array($context, $pks, $value));
+
+ if (\in_array(false, $result, true))
+ {
+ $this->setError($table->getError());
+
+ return false;
+ }
+
try
{
$db = $this->getDbo();
@@ -942,6 +997,16 @@ public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = n
$table->reorder();
+ // Trigger the change state event.
+ $result = Factory::getApplication()->triggerEvent($this->event_after_change_featured, array($context, $pks, $value));
+
+ if (\in_array(false, $result, true))
+ {
+ $this->setError($table->getError());
+
+ return false;
+ }
+
$this->cleanCache();
return true;
diff --git a/administrator/components/com_content/tmpl/articles/default.php b/administrator/components/com_content/tmpl/articles/default.php
index fa443112a0afc..12cda5c5d8360 100644
--- a/administrator/components/com_content/tmpl/articles/default.php
+++ b/administrator/components/com_content/tmpl/articles/default.php
@@ -205,6 +205,10 @@
'disabled' => !$canChange
];
+ if ($workflow_enabled) :
+ $options['disabled'] = true;
+ endif;
+
echo (new FeaturedButton)
->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down);
?>
diff --git a/administrator/components/com_content/tmpl/featured/default.php b/administrator/components/com_content/tmpl/featured/default.php
index 8e9e7b54e790f..6d993dcca50eb 100644
--- a/administrator/components/com_content/tmpl/featured/default.php
+++ b/administrator/components/com_content/tmpl/featured/default.php
@@ -197,6 +197,10 @@
'disabled' => !$canChange
];
+ if ($workflow_enabled) :
+ $options['disabled'] = true;
+ endif;
+
echo (new FeaturedButton)
->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down);
?>
diff --git a/administrator/language/en-GB/plg_workflow_featuring.ini b/administrator/language/en-GB/plg_workflow_featuring.ini
new file mode 100644
index 0000000000000..f7c2303092ab4
--- /dev/null
+++ b/administrator/language/en-GB/plg_workflow_featuring.ini
@@ -0,0 +1,11 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_WORKFLOW_FEATURING="Workflow - Featuring"
+PLG_WORKFLOW_FEATURING_XML_DESCRIPTION="Add featuring actions to the workflow transitions for your items"
+PLG_WORKFLOW_FEATURING_TRANSITION_ACTIONS_FEATURING_LABEL="Featuring state"
+PLG_WORKFLOW_FEATURING_TRANSITION_ACTIONS_FEATURING_DESC="Define the featured state an item should be set, when executing this transition"
+PLG_WORKFLOW_FEATURING_CHANGE_STATE_NOT_ALLOWED="You're not allowed to change the featured state of this item. Please use a workflow transition."
+PLG_WORKFLOW_FEATURING_FEATURED="Featured: %s"
diff --git a/administrator/language/en-GB/plg_workflow_featuring.sys.ini b/administrator/language/en-GB/plg_workflow_featuring.sys.ini
new file mode 100644
index 0000000000000..a80d79cb2fe80
--- /dev/null
+++ b/administrator/language/en-GB/plg_workflow_featuring.sys.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2019 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_WORKFLOW_FEATURING="Workflow - Featuring"
+PLG_WORKFLOW_FEATURING_XML_DESCRIPTION="Add featuring options to the workflow transitions for your items"
diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql
index fe10e567000d5..86880b10e7530 100644
--- a/installation/sql/mysql/base.sql
+++ b/installation/sql/mysql/base.sql
@@ -358,7 +358,8 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`,
(0, 'plg_media-action_rotate', 'plugin', 'rotate', 'media-action', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
(0, 'plg_system_accessibility', 'plugin', 'accessibility', 'system', 0, 0, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
(0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
-(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0);
+(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
+(0, 'plg_workflow_featuring', 'plugin', 'featuring', 'workflow', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0);
-- Templates
INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `locked`, `manifest_cache`, `params`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES
diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql
index aaf7e5f222378..3655d76239a2b 100644
--- a/installation/sql/postgresql/base.sql
+++ b/installation/sql/postgresql/base.sql
@@ -364,7 +364,8 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder",
(0, 'plg_media-action_rotate', 'plugin', 'rotate', 'media-action', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
(0, 'plg_system_accessibility', 'plugin', 'accessibility', 'system', 0, 0, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
(0, 'plg_system_webauthn', 'plugin', 'webauthn', 'system', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
-(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0);
+(0, 'plg_workflow_publishing', 'plugin', 'publishing', 'workflow', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0),
+(0, 'plg_workflow_featuring', 'plugin', 'featuring', 'workflow', 0, 1, 1, 0, 1, '', '{}', 0, NULL, 0, 0);
-- Templates
INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "locked", "manifest_cache", "params", "checked_out", "checked_out_time", "ordering", "state") VALUES
@@ -1181,4 +1182,4 @@ INSERT INTO "#__workflow_transitions" ("id", "asset_id", "published", "ordering"
(3, 63, 1, 3, 1, 'Trash', '', -1, 3, '{"publishing":"-2"}', NULL, 0),
(4, 64, 1, 4, 1, 'Archive', '', -1, 4, '{"publishing":"2"}', NULL, 0);
-SELECT setval('#__workflow_transitions_id_seq', 5, false);
\ No newline at end of file
+SELECT setval('#__workflow_transitions_id_seq', 5, false);
diff --git a/libraries/src/Workflow/WorkflowServiceTrait.php b/libraries/src/Workflow/WorkflowServiceTrait.php
index 20c3cc6eae4a8..66f8dd0435a6a 100644
--- a/libraries/src/Workflow/WorkflowServiceTrait.php
+++ b/libraries/src/Workflow/WorkflowServiceTrait.php
@@ -31,14 +31,17 @@ trait WorkflowServiceTrait
*/
abstract public function getMVCFactory(): MVCFactoryInterface;
- /** @var array Supported functionality */
- protected $supportedFunctionality = [
- 'joomla.state' => true,
- 'joomla.featured' => true,
- ];
-
/**
- * Check if the functionality is supported by the context
+ * Check if the functionality is supported by the component
+ * The variable $supportFunctionality has the following structure
+ * [
+ * 'core.featured' => [
+ * 'com_content.article',
+ * ],
+ * 'core.state' => [
+ * 'com_content.article',
+ * ],
+ * ]
*
* @param string $functionality The functionality
* @param string $context The context of the functionality
@@ -57,7 +60,7 @@ public function supportFunctionality($functionality, $context): bool
return true;
}
- return in_array($context, $this->supportedFunctionality[$functionality]);
+ return in_array($context, $this->supportedFunctionality[$functionality], true);
}
/**
diff --git a/plugins/workflow/featuring/featuring.php b/plugins/workflow/featuring/featuring.php
new file mode 100644
index 0000000000000..e2216352b9b3f
--- /dev/null
+++ b/plugins/workflow/featuring/featuring.php
@@ -0,0 +1,424 @@
+getName();
+
+ // Extend the transition form
+ if ($context == 'com_workflow.transition')
+ {
+ return $this->enhanceTransitionForm($form, $data);
+ }
+
+ return $this->enhanceItemForm($form, $data);
+ }
+
+ /**
+ * Add different parameter options to the transition view, we need when executing the transition
+ *
+ * @param Form $form The form
+ * @param stdClass $data The data
+ *
+ * @return boolean
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function enhanceTransitionForm(Form $form, $data)
+ {
+ $model = $this->app->bootComponent('com_workflow')
+ ->getMVCFactory()->createModel('Workflow', 'Administrator', ['ignore_request' => true]);
+
+ $workflow_id = (int) ($data->workflow_id ?? $form->getValue('workflow_id'));
+
+ if (empty($workflow_id))
+ {
+ $workflow_id = (int) $this->app->input->getInt('workflow_id');
+ }
+
+ $workflow = $model->getItem($workflow_id);
+
+ if (!$this->isSupported($workflow->extension))
+ {
+ return true;
+ }
+
+ $form->loadFile(__DIR__ . '/forms/action.xml');
+
+ return true;
+ }
+
+ /**
+ * Disable certain fields in the item form view, when we want to take over this function in the transition
+ * Check also for the workflow implementation and if the field exists
+ *
+ * @param Form $form The form
+ * @param stdClass $data The data
+ *
+ * @return boolean
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function enhanceItemForm(Form $form, $data)
+ {
+ $context = $form->getName();
+
+ if (!$this->isSupported($context))
+ {
+ return true;
+ }
+
+ $parts = explode('.', $context);
+
+ $component = $this->app->bootComponent($parts[0]);
+
+ $modelName = $component->getModelName($context);
+
+ $table = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true])
+ ->getTable();
+
+ $fieldname = $table->getColumnAlias('featured');
+
+ $options = $form->getField($fieldname)->options;
+
+ $value = isset($data->$fieldname) ? $data->$fieldname : $form->getValue($fieldname, null, 0);
+
+ $text = '-';
+
+ $textclass = 'body';
+
+ switch ($value)
+ {
+ case 1:
+ $textclass = 'success';
+ break;
+
+ case 0:
+ case -2:
+ $textclass = 'danger';
+ }
+
+ if (!empty($options))
+ {
+ foreach ($options as $option)
+ {
+ if ($option->value == $value)
+ {
+ $text = $option->text;
+
+ break;
+ }
+ }
+ }
+
+ $form->setFieldAttribute($fieldname, 'type', 'spacer');
+
+ $form->setFieldAttribute($fieldname, 'label', Text::sprintf('PLG_WORKFLOW_FEATURING_FEATURED', '' . htmlentities($text, ENT_COMPAT, 'UTF-8') . ''));
+
+ return true;
+ }
+
+ /**
+ * Manipulate the generic list view
+ *
+ * @param string $context
+ * @param ViewInterface $view
+ * @param string $result
+ */
+ public function onAfterDisplay(string $context, ViewInterface $view, string $result)
+ {
+ $parts = explode('.', $context);
+
+ if ($parts < 2)
+ {
+ return true;
+ }
+
+ $app = Factory::getApplication();
+
+ // We need the single model context for checking for workflow
+ $singularsection = Inflector::singularize($parts[1]);
+
+ $newcontext = $parts[0] . '.' . $singularsection;
+
+ if (!$app->isClient('administrator') || !$this->isSupported($newcontext))
+ {
+ return true;
+ }
+
+ // List of releated batch functions we need to hide
+ $states = ['featured', 'unfeatured'];
+
+ $js = "
+ document.addEventListener('DOMContentLoaded', function()
+ {
+ var dropdown = document.getElementById('toolbar-dropdown-status-group');
+
+ if (!dropdown)
+ {
+ reuturn;
+ }
+
+ " . \json_encode($states) . ".forEach((action) => {
+ var button = document.getElementById('status-group-children-' + action);
+
+ if (button)
+ {
+ button.classList.add('d-none');
+ }
+ });
+
+ });
+ ";
+
+ $app->getDocument()->addScriptDeclaration($js);
+
+ return true;
+ }
+
+ /**
+ * Check if we can execute the transition
+ *
+ * @param string $context The context
+ * @param array $pks IDs of the items
+ * @param object $transition The value to change to
+ *
+ * @return boolean
+ */
+ public function onWorkflowBeforeTransition($context, $pks, $transition)
+ {
+ if (!$this->isSupported($context) || !is_numeric($transition->options->get('featuring')))
+ {
+ return true;
+ }
+
+ $value = (int) $transition->options->get('featuring');
+
+ /**
+ * Here it becomes tricky. We would like to use the component models featured method, so we will
+ * Execute the normal "onContentBeforeChangeFeatured" plugins. But they could cancel the execution,
+ * So we have to precheck and cancel the whole transition stuff if not allowed.
+ */
+ $this->app->set('plgWorkflowFeaturing.' . $context, $pks);
+
+ $result = $this->app->triggerEvent('onContentBeforeChangeFeatured', [$context, $pks, $value]);
+
+ // Release whitelist, the job is done
+ $this->app->set('plgWorkflowFeaturing.' . $context, []);
+
+ if (\in_array(false, $result, true))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Change Feature State of an item. Used to disable feature state change
+ *
+ * @param string $context The context
+ * @param array $pks IDs of the items
+ * @param object $transition The value to change to
+ *
+ * @return boolean
+ */
+ public function onWorkflowAfterTransition($context, $pks, $transition)
+ {
+ if (!$this->isSupported($context))
+ {
+ return true;
+ }
+
+ $parts = explode('.', $context);
+
+ // We need at least the extension + view for loading the table fields
+ if (count($parts) < 2)
+ {
+ return false;
+ }
+
+ $component = $this->app->bootComponent($parts[0]);
+
+ $value = (int) $transition->options->get('featuring');
+
+ $options = [
+ 'ignore_request' => true,
+ // We already have triggered onContentBeforeChangeFeatured, so use our own
+ 'event_before_change_featured' => 'onWorkflowBeforeChangeFeatured'
+ ];
+
+ $modelName = $component->getModelName($context);
+
+ $model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), $options);
+
+ return $model->featured($pks, $value);
+ }
+
+ /**
+ * Change Feature State of an item. Used to disable Feature state change
+ *
+ * @param string $context The context
+ * @param array $pks IDs of the items
+ * @param int $value The value to change to
+ * @return boolean
+ */
+ public function onContentBeforeChangeFeatured(string $context, array $pks, int $value): bool
+ {
+ if (!$this->isSupported($context))
+ {
+ return true;
+ }
+
+ // We have whitelisted the pks, so we're the one who triggered
+ // With onWorkflowBeforeTransition => free pass
+ if ($this->app->get('plgWorkflowFeaturing.' . $context) === $pks)
+ {
+ return true;
+ }
+
+ throw new Exception(Text::_('PLG_WORKFLOW_FEATURING_CHANGE_STATE_NOT_ALLOWED'));
+ }
+
+ /**
+ * The save event.
+ *
+ * @param string $context The context
+ * @param object $table The item
+ * @param boolean $isNew Is new item
+ * @param array $data The validated data
+ *
+ * @return boolean
+ *
+ * @since 4.0.0
+ */
+ public function onContentBeforeSave($context, TableInterface $table, $isNew, $data)
+ {
+ if (!$this->isSupported($context))
+ {
+ return true;
+ }
+
+ $keyName = $table->getColumnAlias('featured');
+
+ // Check for the old value
+ $article = clone $table;
+
+ $article->load($table->id);
+
+ // We don't allow the change of the feature state when we use the workflow
+ // As we're setting the field to disabled, no value should be there at all
+ if (isset($data[$keyName]))
+ {
+ $this->app->enqueueMessage(Text::_('PLG_WORKFLOW_FEATURING_CHANGE_STATE_NOT_ALLOWED'), 'error');
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the current plugin should execute workflow related activities
+ *
+ * @param string $context
+ * @return boolean
+ */
+ protected function isSupported($context)
+ {
+ $parts = explode('.', $context);
+
+ // We need at least the extension + view for loading the table fields
+ if (count($parts) < 2)
+ {
+ return false;
+ }
+
+ $component = $this->app->bootComponent($parts[0]);
+
+ if (!$component instanceof WorkflowServiceInterface
+ || !$component->isWorkflowActive($context)
+ || !$component->supportFunctionality($this->supportFunctionality, $context))
+ {
+ return false;
+ }
+
+ $modelName = $component->getModelName($context);
+
+ $model = $component->getMVCFactory()->createModel($modelName, $this->app->getName(), ['ignore_request' => true]);
+
+ if (!$model instanceof DatabaseModelInterface || !method_exists($model, 'featured'))
+ {
+ return false;
+ }
+
+ $table = $model->getTable();
+
+ if (!$table instanceof TableInterface || !$table->hasField('featured'))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/workflow/featuring/featuring.xml b/plugins/workflow/featuring/featuring.xml
new file mode 100644
index 0000000000000..39ab3f4e2e31f
--- /dev/null
+++ b/plugins/workflow/featuring/featuring.xml
@@ -0,0 +1,27 @@
+
+