diff --git a/src/Controller/Component/ConflictListBuilder.php b/src/Controller/Component/ConflictListBuilder.php new file mode 100644 index 0000000..5a8c9ef --- /dev/null +++ b/src/Controller/Component/ConflictListBuilder.php @@ -0,0 +1,224 @@ +conflictTracker = $conflict_tracker; + $this->entityIndex = $entity_index; + $this->entityTypeManager = $entity_type_manager; + $this->dateFormatter = $date_formatter; + } + + /** + * Instantiates a new instance of this list builder. + * + * Because we don't have a single entity type, we cannot use + * EntityHandlerInterface::createInstance. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The service container this object should use. + * + * @return static + * A new instance of this list builder. + */ + public static function createInstance(ContainerInterface $container) { + return new self( + $container->get('workspace.conflict_tracker'), + $container->get('multiversion.entity_index.rev'), + $container->get('entity_type.manager'), + $container->get('date.formatter') + ); + } + + /** + * Build the table header. + * + * @return array + * The header array used by table render arrays. + */ + public function buildHeader() { + $header = array( + 'title' => $this->t('Title'), + 'type' => array( + 'data' => $this->t('Content type'), + 'class' => array(RESPONSIVE_PRIORITY_MEDIUM), + ), + 'author' => array( + 'data' => $this->t('Author'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ), + 'status' => $this->t('Status'), + 'changed' => array( + 'data' => $this->t('Updated'), + 'class' => array(RESPONSIVE_PRIORITY_LOW), + ), + ); + return $header; + } + + /** + * Build a row for the given entity. + * + * @todo Handle translations. + * + * @see \Drupal\node\NodeListBuilder::buildRow + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to build the render array row for. + * + * @return array + * A row render array used by a table render array. + */ + public function buildRow(EntityInterface $entity) { + $entity_type = $entity->getEntityType(); + + $row['title'] = $entity->label(); + + // @todo Is there a way to get the human readable name for the bundle? + $row['type'] = $entity->bundle(); + + $uid_key = $entity_type->getKey('uid'); + if ($uid_key) { + $row['author']['data'] = [ + '#theme' => 'username', + '#account' => $entity->get($uid_key)->entity, + ]; + } + else { + $row['author'] = $this->t('None'); + } + + $status_key = $entity_type->getKey('status'); + if ($status_key) { + $row['status'] = $entity->get($status_key)->value ? $this->t('published') : $this->t('not published'); + } + else { + $row['status'] = $this->t('published'); + } + + // @todo Is there an entity key for changed time? + $row['changed'] = $this->dateFormatter->format($entity->getChangedTime(), 'short'); + + return $row; + } + + /** + * Get the title of the entire table. + * + * @return string + * The title to use for the whole table. + */ + public function getTitle() { + return ''; + } + + /** + * Load the entities needed for the table. + * + * @see workspace_preprocess_workspace_rev + * + * @return \Drupal\Core\Entity\EntityInterface[] + * An array of entities. + */ + public function load($workspace_id) { + /* \Drupal\multiversion\Entity\Workspace $workspace */ + $workspace = Workspace::load($workspace_id); + + $conflicts = $this->conflictTracker + ->useWorkspace($workspace) + ->getAll(); + + $entity_revisions = []; + foreach ($conflicts as $uuid => $conflict) { + // @todo figure out why this is an array and what to do if there is more than 1 + // @todo what happens when the conflict value is not "available"? what does this mean? + $rev = reset(array_keys($conflict)); + $rev_info = $this->entityIndex + ->useWorkspace($workspace->id()) + ->get("$uuid:$rev"); + + if (!empty($rev_info['revision_id'])) { + $entity_revisions[] = $this->entityTypeManager + ->getStorage($rev_info['entity_type_id']) + ->useWorkspace($workspace->id()) + ->loadRevision($rev_info['revision_id']); + } + } + + return $entity_revisions; + } + + /** + * Build the render array to display on the page. + * + * @param string $workspace_id + * The workspace ID to build the conflict list for. + * + * @return array + * A table render array to show on the page. + */ + public function buildList($workspace_id) { + $build['table'] = array( + '#type' => 'table', + '#header' => $this->buildHeader(), + '#title' => $this->getTitle(), + '#rows' => array(), + '#empty' => 'There are no conflicts.', + ); + + $entities = $this->load($workspace_id); + foreach ($entities as $entity) { + if ($row = $this->buildRow($entity)) { + $build['table']['#rows'][$entity->id()] = $row; + } + } + + // Only add the pager if a limit is specified. + if ($this->limit) { + $build['pager'] = array( + '#type' => 'pager', + ); + } + + return $build; + } + +} diff --git a/src/Controller/WorkspaceController.php b/src/Controller/WorkspaceController.php index fc74889..9a54748 100644 --- a/src/Controller/WorkspaceController.php +++ b/src/Controller/WorkspaceController.php @@ -7,6 +7,7 @@ use Drupal\Core\Url; use Drupal\multiversion\Entity\Workspace; use Drupal\multiversion\Entity\WorkspaceType; use Drupal\multiversion\Entity\WorkspaceTypeInterface; +use Drupal\workspace\Controller\Component\ConflictListBuilder; class WorkspaceController extends ControllerBase { @@ -38,4 +39,30 @@ class WorkspaceController extends ControllerBase { public function getAddFormTitle(WorkspaceTypeInterface $workspace_type) { return $this->t('Add %type workspace', array('%type' => $workspace_type->label())); } + + /** + * View a list of conflicts for a workspace. + * + * @param string $workspace + * The workspace ID to get conflicts for. + * + * @return array + * The render array to display for the page. + */ + public function viewConflicts($workspace) { + $container = \Drupal::getContainer(); + $builder = ConflictListBuilder::createInstance($container); + return $builder->buildList($workspace); + } + + /** + * Get the page title for the list of conflicts page. + * + * @return string + * The page title. + */ + public function getViewConflictsTitle() { + return 'Workspace Conflicts'; + } + } diff --git a/src/Entity/Form/WorkspaceForm.php b/src/Entity/Form/WorkspaceForm.php index 929a487..5d16afd 100644 --- a/src/Entity/Form/WorkspaceForm.php +++ b/src/Entity/Form/WorkspaceForm.php @@ -5,6 +5,10 @@ namespace Drupal\workspace\Entity\Form; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\Url; +use Drupal\multiversion\Workspace\ConflictTrackerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Form controller for the workspace edit forms. @@ -12,6 +16,13 @@ use Drupal\Core\Form\FormStateInterface; class WorkspaceForm extends ContentEntityForm { /** + * The injected service to track conflicts during replication. + * + * @var ConflictTrackerInterface + */ + protected $conflictTracker; + + /** * The workspace content entity. * * @var \Drupal\multiversion\Entity\WorkspaceInterface @@ -19,14 +30,62 @@ class WorkspaceForm extends ContentEntityForm { protected $entity; /** + * Constructs a ContentEntityForm object. + * + * @param ConflictTrackerInterface $conflict_tracker + * The confict tracking service. + */ + public function __construct(ConflictTrackerInterface $conflict_tracker) { + $this->conflictTracker = $conflict_tracker; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('workspace.conflict_tracker') + ); + } + + /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { $workspace = $this->entity; if ($this->operation == 'edit') { + // Allow the user to not abort on conflicts. + $this->conflictTracker->useWorkspace($workspace); + $conflicts = $this->conflictTracker->getAll(); + if ($conflicts) { + $form['message'] = $this->generateMessageRenderArray('error', $this->t( + 'There are @count conflict(s) with the :target workspace. Pushing changes to :target may result in unexpected behavior or data loss, and cannot be undone. Please proceed with caution.', + [ + '@count' => count($conflicts), + ':link' => Url::fromRoute('entity.workspace.conflicts', ['workspace' => $workspace->id()])->toString(), + ':target' => $workspace->get('upstream')->entity->label(), + ] + )); + $form['is_aborted_on_conflict'] = [ + '#type' => 'radios', + '#title' => $this->t('Abort if there are conflicts?'), + '#default_value' => 'true', + '#options' => [ + 'true' => $this->t('Yes, if conflicts are found do not replicate to upstream.'), + 'false' => $this->t('No, go ahead and push any conflicts to the upstream.'), + ], + '#weight' => 0, + ]; + } + else { + $form['message'] = $this->generateMessageRenderArray('status', 'There are no conflicts.'); + } + + // Set the form title based on workspace. $form['#title'] = $this->t('Edit workspace %label', array('%label' => $workspace->label())); } + $form['label'] = array( '#type' => 'textfield', '#title' => $this->t('Label'), @@ -69,7 +128,7 @@ class WorkspaceForm extends ContentEntityForm { // contained in the display. $field_names = array( 'label', - 'machine_name' + 'machine_name', ); foreach ($violations->getByFields($field_names) as $violation) { list($field_name) = explode('.', $violation->getPropertyPath(), 2); @@ -82,32 +141,106 @@ class WorkspaceForm extends ContentEntityForm { * {@inheritdoc} */ public function save(array $form, FormStateInterface $form_state) { + // Pass the abort flag to the ReplicationManager using runtime-only state, + // i.e. a static. + // @see \Drupal\workspace\ReplicatorManager + $is_aborted_on_conflict = !$form_state->hasValue('is_aborted_on_conflict') || $form_state->getValue('is_aborted_on_conflict') === 'true'; + drupal_static('workspace_is_aborted_on_conflict', $is_aborted_on_conflict); + $workspace = $this->entity; - $insert = $workspace->isNew(); + $is_new = $workspace->isNew(); $workspace->save(); + $info = ['%info' => $workspace->label()]; $context = array('@type' => $workspace->bundle(), '%info' => $workspace->label()); $logger = $this->logger('workspace'); - if ($insert) { - $logger->notice('@type: added %info.', $context); - drupal_set_message($this->t('Workspace %info has been created.', $info)); - } - else { + // If Workbench Moderation is enabled, a publish of the Workspace should + // trigger a replication. We pass back the status of that replication using + // a static variable. If replication happened, we want to handle the case + // of failed replication, as well as modify the wording of the saved + // message. + // @see \Drupal\workspace\EventSubscriber\WorkbenchModerationSubscriber + // @todo Avoid using statics. + $replication_status = drupal_static('publish_workspace_replication_status', NULL); + if ($replication_status !== NULL) { $logger->notice('@type: updated %info.', $context); - drupal_set_message($this->t('Workspace %info has been updated.', $info)); - } - if ($workspace->id()) { - $form_state->setValue('id', $workspace->id()); - $form_state->set('id', $workspace->id()); - $redirect = $this->currentUser()->hasPermission('administer workspaces') ? $workspace->toUrl('collection') : $workspace->toUrl('canonical'); - $form_state->setRedirectUrl($redirect); + if ($replication_status == TRUE) { + // The replication succeeded, in addition to saving the workspace. + drupal_set_message($this->t('Workspace :source has been updated and changes were pushed to :target.', [ + ':source' => $workspace->label(), + ':target' => $workspace->get('upstream')->entity->label(), + ]), 'status'); + + $form_state->setValue('id', $workspace->id()); + $form_state->set('id', $workspace->id()); + $redirect = $this->currentUser()->hasPermission('administer workspaces') ? $workspace->toUrl('collection') : $workspace->toUrl('canonical'); + $form_state->setRedirectUrl($redirect); + } + else { + // The replication failed, even though the Workspace was updated. + $previous_workflow_state = drupal_static('publish_workspace_previous_state', NULL); + + // This variable should always be set, else there is an issue with + // the trigger logic. + if ($previous_workflow_state === NULL) { + throw new \Exception('The publish_workspace_replication_status should be set.'); + } + + // Revert the workspace back to its previous moderation state. + $workspace->moderation_state->target_id = $previous_workflow_state; + $workspace->save(); + + // Show the form again. + $form_state->setRebuild(); + } } else { - drupal_set_message($this->t('The workspace could not be saved.'), 'error'); - $form_state->setRebuild(); + // Assume a replication did not happen OR that Workbench Moderation is not + // installed. + if ($is_new) { + $logger->notice('@type: added %info.', $context); + drupal_set_message($this->t('Workspace %info has been created.', $info)); + } + else { + $logger->notice('@type: updated %info.', $context); + drupal_set_message($this->t('Workspace %info has been updated.', $info)); + } + + if ($workspace->id()) { + $form_state->setValue('id', $workspace->id()); + $form_state->set('id', $workspace->id()); + $redirect = $this->currentUser()->hasPermission('administer workspaces') ? $workspace->toUrl('collection') : $workspace->toUrl('canonical'); + $form_state->setRedirectUrl($redirect); + } + else { + drupal_set_message($this->t('The workspace could not be saved.'), 'error'); + $form_state->setRebuild(); + } } } + /** + * Generate a message render array with the given text. + * + * @param string $type + * The type of message: status, warning, or error. + * @param string $message + * The message to create with. + * + * @return array + * The render array for a status message. + * + * @see \Drupal\Core\Render\Element\StatusMessages + */ + protected function generateMessageRenderArray($type, $message) { + return [ + '#theme' => 'status_messages', + '#message_list' => [ + $type => [Markup::create($message)], + ], + ]; + } + } diff --git a/src/EntityTypeInfo.php b/src/EntityTypeInfo.php index 0f38520..d303dfb 100644 --- a/src/EntityTypeInfo.php +++ b/src/EntityTypeInfo.php @@ -108,6 +108,7 @@ class EntityTypeInfo { $workspace->setLinkTemplate('canonical', '/admin/structure/workspace/{workspace}'); $workspace->setLinkTemplate('edit-form', '/admin/structure/workspace/{workspace}/edit'); $workspace->setLinkTemplate('activate-form', '/admin/structure/workspace/{workspace}/activate'); + $workspace->setLinkTemplate('conflicts', '/admin/structure/workspace/{workspace}/conflicts'); $workspace->set('field_ui_base_route', 'entity.workspace_type.edit_form'); return $workspace; diff --git a/src/EventSubscriber/WorkbenchModerationSubscriber.php b/src/EventSubscriber/WorkbenchModerationSubscriber.php index 603b44a..9475220 100644 --- a/src/EventSubscriber/WorkbenchModerationSubscriber.php +++ b/src/EventSubscriber/WorkbenchModerationSubscriber.php @@ -48,11 +48,35 @@ class WorkbenchModerationSubscriber implements EventSubscriberInterface { * The transition event that just fired. */ public function onTransition(WorkbenchModerationTransitionEvent $event) { + /* @var WorkspaceInterface $entity */ $entity = $event->getEntity(); if ($entity->getEntityTypeId() == 'workspace' && $this->wasDefaultRevision($event)) { - /** @var WorkspaceInterface $entity */ - $this->mergeWorkspaceToParent($entity); + + // If there is no upstream to replicate to, abort. + if (!$entity->get('upstream')->entity) { + drupal_set_message(t('The :source workspace does not have an upstream to replicate to!', [ + ':source' => $entity->label(), + ]), 'error'); + + // @todo Should we revert the workspace to its previous state? + + return; + } + + $log = $this->mergeWorkspaceToParent($entity); + + // Pass the replication status to the logic that triggered the state + // change. This allows, for example, the caller to revert back the + // Workspace's workflow state. + // @see \Drupal\workspace\Entity\Form\WorkspaceForm + drupal_static('publish_workspace_replication_status', (bool) $log->get('ok')->value); + + // Set the previous workflow state in case a revert needs to happen. + // Note: we would not be able to revert back the Workspace's moderation + // state here since the event is triggered within a presave hook. + // @todo Find a way to share the replication pass/fail status besides a static. + drupal_static('publish_workspace_previous_state', $event->getStateBefore()); } } @@ -77,22 +101,21 @@ class WorkbenchModerationSubscriber implements EventSubscriberInterface { * * @param \Drupal\multiversion\Entity\WorkspaceInterface $workspace * The workspace entity to merge. + * + * @return \Drupal\replication\Entity\ReplicationLog + * The replication log entry. */ protected function mergeWorkspaceToParent(WorkspaceInterface $workspace) { - // This may be insufficient for handling a missing parent. - /** @var \Drupal\workspace\WorkspacePointerInterface $parent_workspace */ + /* @var \Drupal\workspace\WorkspacePointerInterface $parent_workspace */ $parent_workspace_pointer = $workspace->get('upstream')->entity; - if (!$parent_workspace_pointer) { - // @todo Should we silently ignore this, or throw an error, or...? - return; - } + /* @var \Drupal\workspace\WorkspacePointerInterface $source_pointer */ $source_pointer = $this->getPointerToWorkspace($workspace); // Derive a replication task from the Workspace we are acting on. $task = $this->replicatorManager->getTask($workspace, 'push_replication_settings'); - $this->replicatorManager->replicate($source_pointer, $parent_workspace_pointer, $task); + return $this->replicatorManager->replicate($source_pointer, $parent_workspace_pointer, $task); } /** diff --git a/src/Form/UpdateForm.php b/src/Form/UpdateForm.php index df7035d..7e2afc5 100644 --- a/src/Form/UpdateForm.php +++ b/src/Form/UpdateForm.php @@ -163,7 +163,7 @@ class UpdateForm extends ConfirmFormBase { $response = $this->replicatorManager->update($upstream, $active, $task); - if (($response instanceof ReplicationLogInterface) && $response->get('ok')) { + if (($response instanceof ReplicationLogInterface) && ($response->get('ok')->value === TRUE)) { drupal_set_message($this->t('%workspace has been updated with content from %upstream.', ['%upstream' => $upstream->label(), '%workspace' => $active->label()])); } else { diff --git a/src/InternalReplicator.php b/src/InternalReplicator.php index 65d8c93..4a8c9b7 100644 --- a/src/InternalReplicator.php +++ b/src/InternalReplicator.php @@ -93,6 +93,8 @@ class InternalReplicator implements ReplicatorInterface { $source_workspace = $source->getWorkspace(); $target_workspace = $target->getWorkspace(); // Set active workspace to source. + // @todo Avoid modifying the user's active workspace. + $current_active = $this->workspaceManager->getActiveWorkspace(); try { $this->workspaceManager->setActiveWorkspace($source_workspace); } @@ -177,6 +179,10 @@ class InternalReplicator implements ReplicatorInterface { $replication_log->setSourceLastSeq($source_workspace->getUpdateSeq()); $replication_log->setHistory($history); $replication_log->save(); + + // Switch back to the workspace that was originally active. + $this->workspaceManager->setActiveWorkspace($current_active); + return $replication_log; } diff --git a/src/Plugin/RulesAction/ReplicateContent.php b/src/Plugin/RulesAction/ReplicateContent.php index 8431b49..c72a72e 100644 --- a/src/Plugin/RulesAction/ReplicateContent.php +++ b/src/Plugin/RulesAction/ReplicateContent.php @@ -99,7 +99,7 @@ class ReplicateContent extends RulesActionBase implements ContainerFactoryPlugin /** @var \Drupal\replication\Entity\ReplicationLogInterface $result */ $result = $this->replicatorManager->replicate($source, $upstream, $task); - if ($result->get('ok') == TRUE) { + if ($result->get('ok')->value === TRUE) { drupal_set_message($this->t('Content replicated from workspace @source to workspace @upstream.', ['@source' => $workspace->label(), '@upstream' => $upstream->label()])); } diff --git a/src/ReplicatorManager.php b/src/ReplicatorManager.php index 15f5c1b..a294c0b 100644 --- a/src/ReplicatorManager.php +++ b/src/ReplicatorManager.php @@ -9,6 +9,7 @@ use Drupal\replication\Entity\ReplicationLog; use Drupal\replication\ReplicationTask\ReplicationTask; use Drupal\replication\ReplicationTask\ReplicationTaskInterface; use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Provides the Replicator manager. @@ -30,13 +31,23 @@ class ReplicatorManager implements ReplicatorInterface { protected $conflictTracker; /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** * The injected service to track conflicts during replication. * * @param ConflictTrackerInterface $conflict_tracker * The confict tracking service. + * @param EventDispatcherInterface $event_dispatcher + * The event dispatcher. */ - public function __construct(ConflictTrackerInterface $conflict_tracker) { + public function __construct(ConflictTrackerInterface $conflict_tracker, EventDispatcherInterface $event_dispatcher) { $this->conflictTracker = $conflict_tracker; + $this->eventDispatcher = $event_dispatcher; } /** @@ -60,8 +71,21 @@ class ReplicatorManager implements ReplicatorInterface { * {@inheritdoc} */ public function replicate(WorkspacePointerInterface $source, WorkspacePointerInterface $target, ReplicationTaskInterface $task = NULL) { - // @todo use $initial_conflicts in a conflict management workflow - $initial_conflicts = $this->conflictTracker->getAll(); + // It is assumed a caller of replicate will set this static variable to + // FALSE if they wish to proceed with replicating content upstream even in + // the presence of conflicts. If the caller wants to make sure no conflicts + // are replicated to the upstream, set this value to TRUE. + // By default, the value is FALSE so as not to break the previous + // behavior. + // @todo Use a sequence index instead of boolean? This will allow the + // caller to know there haven't been additional conflicts. + $is_aborted_on_conflict = drupal_static('workspace_is_aborted_on_conflict', FALSE); + + // Abort updating the Workspace if there are conflicts. + $initial_conflicts = $this->conflictTracker->useWorkspace($source->getWorkspace())->getAll(); + if ($is_aborted_on_conflict && $initial_conflicts) { + return $this->failedReplicationLog($source, $target, $task); + } // Derive a pull replication task from the Workspace we are acting on. $pull_task = $this->getTask($source->getWorkspace(), 'pull_replication_settings'); @@ -69,8 +93,11 @@ class ReplicatorManager implements ReplicatorInterface { // Pull in changes from $target to $source to ensure a merge will complete. $this->update($target, $source, $pull_task); - // @todo use $post_conflicts in a conflict management workflow - $post_conflicts = $this->conflictTracker->getAll(); + // Abort replicating to target Workspace if there are conflicts. + $post_conflicts = $this->conflictTracker->useWorkspace($source->getWorkspace())->getAll(); + if ($is_aborted_on_conflict && $post_conflicts) { + return $this->failedReplicationLog($source, $target, $task); + } // Automatically derive settings from the workspace if no task sent. // @todo Refactor to eliminate obscurity of having an optional parameter @@ -156,7 +183,30 @@ class ReplicatorManager implements ReplicatorInterface { protected function doReplication(WorkspacePointerInterface $source, WorkspacePointerInterface $target, ReplicationTaskInterface $task = NULL) { foreach ($this->replicators as $replicator) { if ($replicator->applies($source, $target)) { - return $replicator->replicate($source, $target, $task); + // @TODO: Get rid of this meta-programming once #2814055 lands in + // Replication. + $events_class = '\Drupal\replication\Event\ReplicationEvents'; + $event_class = '\Drupal\replication\Event\ReplicationEvent'; + + if (class_exists($events_class) && class_exists($event_class)) { + $event = new $event_class($source->getWorkspace(), $target->getWorkspace()); + } + + // Dispatch the pre-replication event, if the event object exists. + if (isset($event)) { + $this->eventDispatcher->dispatch($events_class::PRE_REPLICATION, $event); + } + + // Do the mysterious dance of replication... + $log = $replicator->replicate($source, $target, $task); + + // ...and dispatch the post-replication event, if the event object + // exists. + if (isset($event)) { + $this->eventDispatcher->dispatch($events_class::POST_REPLICATION, $event); + } + + return $log; } } diff --git a/src/WorkspaceListBuilder.php b/src/WorkspaceListBuilder.php index 31a5a31..9bf91af 100644 --- a/src/WorkspaceListBuilder.php +++ b/src/WorkspaceListBuilder.php @@ -6,8 +6,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityListBuilder; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\multiversion\Entity\WorkspaceInterface; -use Drupal\multiversion\Entity\WorkspaceTypeInterface; use Drupal\multiversion\Workspace\WorkspaceManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -37,6 +35,7 @@ class WorkspaceListBuilder extends EntityListBuilder { * @param \Drupal\Core\Entity\EntityStorageInterface $storage * The entity storage class. * @param \Drupal\multiversion\Workspace\WorkspaceManagerInterface $workspace_manager + * The workspace manager. */ public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, WorkspaceManagerInterface $workspace_manager) { parent::__construct($entity_type, $storage); @@ -47,6 +46,7 @@ class WorkspaceListBuilder extends EntityListBuilder { * {@inheritdoc} */ public function buildHeader() { + // @todo should we show conflicted state in this list? $header['label'] = t('Workspace'); $header['uid'] = t('Owner'); $header['type'] = t('Type'); @@ -59,10 +59,10 @@ class WorkspaceListBuilder extends EntityListBuilder { * {@inheritdoc} */ public function buildRow(EntityInterface $entity) { - /** @var WorkspaceInterface $entity */ + /** @var \Drupal\multiversion\Entity\WorkspaceInterface $entity */ $row['label'] = $entity->label() . ' (' . $entity->getMachineName() . ')'; $row['owner'] = $entity->getOwner()->getDisplayname(); - /** @var WorkspaceTypeInterface $type */ + /** @var \Drupal\multiversion\Entity\WorkspaceTypeInterface $type */ $type = $entity->get('type')->first()->entity; $row['type'] = $type ? $type->label() : ''; $active_workspace = $this->workspaceManager->getActiveWorkspace()->id(); @@ -74,10 +74,10 @@ class WorkspaceListBuilder extends EntityListBuilder { * {@inheritdoc} */ public function getDefaultOperations(EntityInterface $entity) { - /** @var WorkspaceInterface $entity */ + /** @var \Drupal\multiversion\Entity\WorkspaceInterface $entity */ $operations = parent::getDefaultOperations($entity); if (isset($operations['edit'])) { - $operations['edit']['query']['destination'] = $entity->url('collection'); + $operations['edit']['query']['destination'] = $entity->toUrl('collection'); } $active_workspace = $this->workspaceManager->getActiveWorkspace()->id(); @@ -85,11 +85,17 @@ class WorkspaceListBuilder extends EntityListBuilder { $operations['activate'] = array( 'title' => $this->t('Set Active'), 'weight' => 20, - 'url' => $entity->urlInfo('activate-form', ['query' => ['destination' => $entity->url('collection')]]), + 'url' => $entity->toUrl('activate-form', ['query' => ['destination' => $entity->toUrl('collection')]]), ); } + $operations['conflicts'] = [ + 'title' => $this->t('View Conflicts'), + 'weight' => 21, + 'url' => $entity->toUrl('conflicts', ['workspace' => $entity->id()]), + ]; + return $operations; } -} \ No newline at end of file +} diff --git a/workspace.routing.yml b/workspace.routing.yml index 6577bac..2c68bdb 100644 --- a/workspace.routing.yml +++ b/workspace.routing.yml @@ -37,6 +37,16 @@ entity.workspace.activate_form: requirements: _workspace_view: 'TRUE' +entity.workspace.conflicts: + path: '/admin/structure/workspace/{workspace}/conflicts' + defaults: + _controller: '\Drupal\workspace\Controller\WorkspaceController::viewConflicts' + _title_callback: '\Drupal\workspace\Controller\WorkspaceController::getViewConflictsTitle' + options: + _admin_route: TRUE + requirements: + _permission: 'view_any_workspace' + # WorkspaceType routing definition entity.workspace_type.collection: path: '/admin/structure/workspace/types' diff --git a/workspace.services.yml b/workspace.services.yml index 572bcda..19876a9 100644 --- a/workspace.services.yml +++ b/workspace.services.yml @@ -1,7 +1,7 @@ services: workspace.replicator_manager: class: Drupal\workspace\ReplicatorManager - arguments: ['@workspace.conflict_tracker'] + arguments: ['@workspace.conflict_tracker', '@event_dispatcher'] tags: - { name: service_collector, tag: workspace_replicator, call: addReplicator} workspace.internal_replicator: