4

In SXA 10 I can see a new way to use workflows on datasource items of a page. This is described here.

  1. If I assign the page template to a workflow and set it up as in the documentation, it does not automatically start the workflow on the datasource items. How do I configure this?

  2. I have multiple languages on my site. If I manually start the workflow on a particular language that is not the system default of 'en', the workflow doesn't progress. What's going on here?

2 Answers 2

7

Sitecore support advised the following:

1.

Indeed, currently it is needed to manually assign the workflow to datasource items. I have registered a wish for future versions of SXA to improve this functionality, so that the workflow for datasource items is started automatically. To track the future status of this request, please use the reference number 446319.

This is registered this as a bug in the current version of Sitecore XP. To track the future status of this bug report, please use the reference number 446345.

For now, I have manually overridden the ApplyForDataSourceItemsAction to fix these two issues:

public class ApplyForDataSourceItemsAction
{
    private Dictionary<string, RuleList<RuleContext>> _rules = new Dictionary<string, RuleList<RuleContext>>();

    protected IContentRepository ContentRepository = ServiceLocator.ServiceProvider.GetService<IContentRepository>();

    protected IRulesService RulesService = ServiceLocator.ServiceProvider.GetService<IRulesService>();

    public void Process(WorkflowPipelineArgs args)
    {
        Item innerItem = args.ProcessorItem.InnerItem;
        if (innerItem != null)
        {
            string rulesRawValue = innerItem[Sitecore.XA.Foundation.Editing.Templates.DatasourceWorkflowAction.Fields.ItemValidationRule];
            if (ShouldProcess(args.DataItem, innerItem, rulesRawValue))
            {
                List<Item> renderingDataSourceItems = GetRenderingDataSourceItems(args.DataItem);
                var workflow = WorkflowUtility.GetWorkflow(args.DataItem);
                Item customCommandItem = GetCustomCommandItem(args);
                string rulesRawValue2 = innerItem[Sitecore.XA.Foundation.Editing.Templates.DatasourceWorkflowAction.Fields.DatasourceValidationRule];
                foreach (Item item in ItemUtility.FilterSameItems(renderingDataSourceItems))
                {
                    if (item.Access.CanWrite() && (!item.Locking.IsLocked() || item.Locking.HasLock()) && ShouldProcess(innerItem, item, rulesRawValue2))
                    {
                        var workflowResult = WorkflowUtility.ExecuteWorkflowCommandIfAvailable(item, customCommandItem ?? args.CommandItem, args.CommentFields);
                        if (workflowResult == null || !workflowResult.Succeeded)
                        {
                            workflow.Start(item);
                        }
                    }
                }
            }
        }
    }

    protected virtual bool ShouldProcess(Item contextItem, Item actionItem, string rulesRawValue)
    {
        if (string.IsNullOrWhiteSpace(rulesRawValue))
        {
            return true;
        }

        RuleList<RuleContext> rules = GetRules(rulesRawValue);
        if (rules == null)
        {
            return true;
        }

        return RulesService.EvaluateRules(rules, new RuleContext
        {
            Item = contextItem
        });
    }

    protected virtual RuleList<RuleContext> GetRules(string rulesRawValue)
    {
        if (!_rules.ContainsKey(rulesRawValue))
        {
            RuleList<RuleContext> value = RulesService.ParseRules(rulesRawValue);
            _rules.Add(rulesRawValue, value);
        }

        return _rules[rulesRawValue];
    }

    protected virtual Item GetCustomCommandItem(WorkflowPipelineArgs args)
    {
        Item innerItem = args.ProcessorItem.InnerItem;
        if (innerItem == null)
        {
            return null;
        }

        string value = innerItem[Sitecore.XA.Foundation.Editing.Templates.DatasourceWorkflowAction.Fields.CommandItem];
        if (string.IsNullOrWhiteSpace(value) || !ID.TryParse(value, out ID result))
        {
            return null;
        }

        Item item = ContentRepository.GetItem(result);
        if (item == null)
        {
            return null;
        }

        if (item.DoesItemInheritFrom(Sitecore.XA.Foundation.Editing.Templates.Command))
        {
            return item;
        }

        return null;
    }

    protected virtual List<Item> GetRenderingDataSourceItems(Item item)
    {
        List<Item> list = new List<Item>();
        RenderingReference[] renderings = GetRenderings(item);
        BaseCorePipelineManager service = ServiceLocator.ServiceProvider.GetService<BaseCorePipelineManager>();
        IContentRepository service2 = ServiceLocator.ServiceProvider.GetService<IContentRepository>();
        RenderingReference[] array = renderings;
        for (int j = 0; j < array.Length; j++)
        {
            ResolveRenderingDatasourceArgs resolveRenderingDatasourceArgs = new ResolveRenderingDatasourceArgs(array[j].Settings.DataSource);
            resolveRenderingDatasourceArgs.CustomData["contextItem"] = item;
            using (new SiteContextSwitcher(new SiteContext(ServiceLocator.ServiceProvider.GetService<ISiteInfoResolver>().GetSiteInfo(item))))
            {
                service.Run("resolveRenderingDatasource", resolveRenderingDatasourceArgs);
            }

            if (!string.IsNullOrWhiteSpace(resolveRenderingDatasourceArgs.Datasource))
            {
                List<Item> list2 = (from i in new ListString(resolveRenderingDatasourceArgs.Datasource).Items
                                    .Select(x => ContentRepository.GetItem(x, item.Language ?? Sitecore.Context.Language))
                                    where i != null && i.Versions.GetVersionNumbers().Length != 0
                                    select i).ToList();
                if (list2.Any())
                {
                    foreach (Item item3 in list2)
                    {
                        Item item2 = service2.GetItem(item3.ID, item.Language ?? Sitecore.Context.Language) ?? item3;
                        list.Add(item2);
                        list.AddRange(item2.Children.Where((Item i) => i.Template.DoesTemplateInheritFrom(Sitecore.XA.Foundation.Multisite.Templates.Page.ID)));
                    }
                }
            }
        }

        return list;
    }

    protected virtual RenderingReference[] GetRenderings(Item item)
    {
        using (new ContextItemSwitcher(item))
        {
            return item.Visualization.GetRenderings(Context.Device, checkLogin: true);
        }
    }
}

Edit: Please see answer from Anton for some additional fixes.

2
  • I see neither 446319 nor 446345 in release notes of Sitecore 10.1 and SXA 10.1. So, I assume it still isn't fixed.
    – Anton
    Commented May 6, 2021 at 13:42
  • There are issues with your code. 1) It will start workflow for children, but will not execute command.(it happens because page and datasource will be in different workflow states). 2) It will take datasource from final state. It can cause unpublishing of datasource that is shared between different pages.
    – Anton
    Commented May 6, 2021 at 17:27
3

Code provided by Mark Gibbons works in general, but it has major 2 issues:

  1. Datasource will not be moved through the workflow if you haven't set initial state.
                    if (item.Access.CanWrite() && (!item.Locking.IsLocked() || item.Locking.HasLock()) && ShouldProcess(innerItem, item, rulesRawValue2))
                    {
                        var workflowResult = WorkflowUtility.ExecuteWorkflowCommandIfAvailable(item, customCommandItem ?? args.CommandItem, args.CommentFields);
                        if (workflowResult == null || !workflowResult.Succeeded)
                        {
                            // Datasource item will be placed to initial state of workflow by executing:
                            workflow.Start(item);
                            // In order to move datasource item through the workflow, you need to execute workflow command one more time:
                            WorkflowUtility.ExecuteWorkflowCommandIfAvailable(item, customCommandItem ?? args.CommandItem, args.CommentFields);
                        }
                    }
  1. If you have your datasource in the final state, it could be taken from that state and placed to the initial workflow state. It is dangerous because if you have datasource that is shared between 2 pages, the second page will not be able to read this item after the next publishing. And even with one page, it still dangerous. In the case you are using versions, it also can be dangerous. For example, your page has 2 versions(one is in the final state), but datasource has one version in the final state. Your page will not be able to get this datasource after publishing. That is why it is important to check if datasouce is not in the final state before executing the workflow command.
var state = workflow.GetState(item);
if (state == null || !state.FinalState)
{
    //Here is the logic of executing an action on datasouce item
}

The whole code will look like this:

    public class ApplyForDataSourceItemsAction
    {
        private Dictionary<string, RuleList<RuleContext>> _rules = new Dictionary<string, RuleList<RuleContext>>();

        protected IContentRepository ContentRepository = ServiceLocator.ServiceProvider.GetService<IContentRepository>();

        protected IRulesService RulesService = ServiceLocator.ServiceProvider.GetService<IRulesService>();

        public void Process(WorkflowPipelineArgs args)
        {
            Item innerItem = args.ProcessorItem.InnerItem;
            if (innerItem != null)
            {
                string rulesRawValue = innerItem[Templates.DatasourceWorkflowAction.Fields.ItemValidationRule];
                if (ShouldProcess(args.DataItem, innerItem, rulesRawValue))
                {
                    List<Item> renderingDataSourceItems = GetRenderingDataSourceItems(args.DataItem);
                    var workflow = WorkflowUtility.GetWorkflow(args.DataItem);
                    Item customCommandItem = GetCustomCommandItem(args);
                    string rulesRawValue2 = innerItem[Templates.DatasourceWorkflowAction.Fields.DatasourceValidationRule];
                    foreach (Item item in ItemUtility.FilterSameItems(renderingDataSourceItems))
                    {
                        if (item.Access.CanWrite() && (!item.Locking.IsLocked() || item.Locking.HasLock()) && ShouldProcess(innerItem, item, rulesRawValue2))
                        {
                            var state = workflow.GetState(item);
                            if (state == null || !state.FinalState)
                            {
                                var workflowResult = WorkflowUtility.ExecuteWorkflowCommandIfAvailable(item,
                                    customCommandItem ?? args.CommandItem, args.CommentFields);
                                if (workflowResult == null || !workflowResult.Succeeded)
                                {
                                    workflow.Start(item);
                                    WorkflowUtility.ExecuteWorkflowCommandIfAvailable(item,
                                        customCommandItem ?? args.CommandItem, args.CommentFields);
                                }
                            }
                        }
                    }
                }
            }
        }

        protected virtual bool ShouldProcess(Item contextItem, Item actionItem, string rulesRawValue)
        {
            if (string.IsNullOrWhiteSpace(rulesRawValue))
            {
                return true;
            }

            RuleList<RuleContext> rules = GetRules(rulesRawValue);
            if (rules == null)
            {
                return true;
            }

            return RulesService.EvaluateRules(rules, new RuleContext
            {
                Item = contextItem
            });
        }

        protected virtual RuleList<RuleContext> GetRules(string rulesRawValue)
        {
            if (!_rules.ContainsKey(rulesRawValue))
            {
                RuleList<RuleContext> value = RulesService.ParseRules(rulesRawValue);
                _rules.Add(rulesRawValue, value);
            }

            return _rules[rulesRawValue];
        }

        protected virtual Item GetCustomCommandItem(WorkflowPipelineArgs args)
        {
            Item innerItem = args.ProcessorItem.InnerItem;
            if (innerItem == null)
            {
                return null;
            }

            string value = innerItem[Templates.DatasourceWorkflowAction.Fields.CommandItem];
            if (string.IsNullOrWhiteSpace(value) || !ID.TryParse(value, out ID result))
            {
                return null;
            }

            Item item = ContentRepository.GetItem(result);
            if (item == null)
            {
                return null;
            }

            if (item.DoesItemInheritFrom(Templates.Command))
            {
                return item;
            }

            return null;
        }

        protected virtual List<Item> GetRenderingDataSourceItems(Item item)
        {
            List<Item> list = new List<Item>();
            RenderingReference[] renderings = GetRenderings(item);
            BaseCorePipelineManager service = ServiceLocator.ServiceProvider.GetService<BaseCorePipelineManager>();
            IContentRepository service2 = ServiceLocator.ServiceProvider.GetService<IContentRepository>();
            RenderingReference[] array = renderings;
            for (int j = 0; j < array.Length; j++)
            {
                ResolveRenderingDatasourceArgs resolveRenderingDatasourceArgs = new ResolveRenderingDatasourceArgs(array[j].Settings.DataSource);
                resolveRenderingDatasourceArgs.CustomData["contextItem"] = item;
                using (new SiteContextSwitcher(new SiteContext(ServiceLocator.ServiceProvider.GetService<ISiteInfoResolver>().GetSiteInfo(item))))
                {
                    service.Run("resolveRenderingDatasource", resolveRenderingDatasourceArgs);
                }

                if (!string.IsNullOrWhiteSpace(resolveRenderingDatasourceArgs.Datasource))
                {
                    //Need attention, possibly can cause bugs. Sitecore 10.1 version supports ContentRepository.GetItem(path, language)
                    //But 9 version has only ContentRepository.GetItem(path)
                    List<Item> list2 = (from i in new ListString(resolveRenderingDatasourceArgs.Datasource).Items
                                        .Select(x => ContentRepository.GetItem(x))
                                        where i != null && i.Versions.GetVersionNumbers().Length != 0
                                        select i).ToList();
                    if (list2.Any())
                    {
                        foreach (Item item3 in list2)
                        {
                            Item item2 = service2.GetItem(item3.ID, item.Language ?? Sitecore.Context.Language) ?? item3;
                            list.Add(item2);
                            list.AddRange(item2.Children.Where((Item i) => i.Template.DoesTemplateInheritFrom(Sitecore.XA.Foundation.Multisite.Templates.Page.ID)));
                        }
                    }
                }
            }

            return list;
        }

        protected virtual RenderingReference[] GetRenderings(Item item)
        {
            using (new ContextItemSwitcher(item))
            {
                return item.Visualization.GetRenderings(Context.Device, checkLogin: true);
            }
        }
    }
5
  • @Mark Gibbons As my answer is only clarification of yours then if it is possible, can you apply my changes to your answer, please? I will remove my answer in that case. P. S. I write it as answer, not as comment only because of limitations on characters and code formatting in comments.
    – Anton
    Commented May 6, 2021 at 17:51
  • Thanks for the improvements, I think I'll leave your answer here as it is complete enough to be an acceptable answer. Commented May 6, 2021 at 23:14
  • @Anton, Looks like in the current implementation "ShouldProcess" method works properly only when you pass row value of item validation rule, because context item for the rule is item under workflow. But when it runs for datasource validation rule the context item is action item (innerItem). it should be a rendering datasource item. Second argument of "ShouldProcess" method is never used.
    – Vlad Shpak
    Commented May 20, 2021 at 12:23
  • @VladShpak yes, probably. I didn't test the rules part. I was interested only in backporting of ApplyForDataSourceItemsAction workflow action to the 9.3 version of Sitecore.
    – Anton
    Commented May 21, 2021 at 8:53
  • 1
    In case anyone else comes across this issue - it's also worth adding logic to handle scenarios where you have renderings that have options to include current page as the datasource. It resulted in infinite loop without extending the FilterSameItems() method to filter out the page item itself from the child items list :) Commented Feb 7 at 0:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.