3

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?

6

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 May 6 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 May 6 at 17:27
2

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);
            }
        }
    }
4
  • @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 May 6 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. – Mark Gibbons May 6 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 May 20 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 May 21 at 8:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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