private async Task LoadLayout(ProcessorModel model, WorkItemTypeModel wit, AzureDevOpsEndpoint endpoint, string processId) { wit.Layout = (await endpoint.GetApiDefinitionAsync <WorkItemLayout>(new object[] { processId, wit.WorkItemType.ReferenceName }, queryForDetails: false)); foreach (var page in wit.Layout.Pages) { var pageKey = $"{wit.WorkItemType.ReferenceName}::{page.Label}"; if (model.WorkItemPages.ContainsKey(pageKey)) { model.WorkItemPages[pageKey] = page; } else { model.WorkItemPages.Add(pageKey, page); } foreach (var section in page.Sections) { foreach (var group in section.Groups) { var groupKey = $"{wit.WorkItemType.ReferenceName}::{page.Label}::{section.Id}::{group.Label}"; if (model.WorkItemGroups.ContainsKey(groupKey)) { model.WorkItemGroups[groupKey] = group; } else { model.WorkItemGroups.Add(groupKey, group); } foreach (var control in group.Controls) { var controlKey = $"{wit.WorkItemType.ReferenceName}::{page.Label}::{section.Id}::{group.Label}::{control.Id}"; if (model.WorkItemControls.ContainsKey(controlKey)) { model.WorkItemControls[controlKey] = control; } else { model.WorkItemControls.Add(controlKey, control); } } } } } }
private async Task SyncWorkItemType(WorkItemTypeModel sourceWit, string processId) { var targetWit = TargetModel.WorkItemTypes.ContainsKey(sourceWit.WorkItemType.Id) ? TargetModel.WorkItemTypes[sourceWit.WorkItemType.Id] : new(); targetWit.WorkItemType = await Target.SyncDefinition(sourceWit.WorkItemType, targetWit.WorkItemType, processId); foreach (var state in sourceWit.States.Where(x => x.CustomizationType == "custom")) { if (state.StateCategory == "Completed") { Log.LogWarning("Cannot modify [Completed] category on work item state [{0}] on wit type [{1}].", state.Name, sourceWit.WorkItemType.ReferenceName); } else { await SyncDefinitionType <WorkItemState>( TargetModel.WorkItemStates, state, TargetModel.WorkItemStates.Values.FirstOrDefault(x => x.Name == state.Name), processId, targetWit.WorkItemType.ReferenceName); } } foreach (var field in sourceWit.Fields) { var existingField = TargetModel.WorkItemFields.Values.FirstOrDefault(x => x.ReferenceName == field.ReferenceName); //if (existingField == null || (existingField != null && field.Customization != "system")) // I don't think you can modify //{ await SyncDefinitionType <WorkItemTypeField>( TargetModel.WorkItemFields, field, existingField, processId, targetWit.WorkItemType.ReferenceName); //} } foreach (var rule in sourceWit.Rules) { await SyncDefinitionType <WorkItemRule>( TargetModel.WorkItemRules, rule, TargetModel.WorkItemRules.Values.FirstOrDefault(x => x.Name == rule.Name), processId, targetWit.WorkItemType.ReferenceName); } foreach (var behavior in sourceWit.Behaviors) { await SyncDefinitionType <WorkItemTypeBehavior>( TargetModel.WorkItemTypeBehaviors, behavior, targetWit.Behaviors.FirstOrDefault(x => x.Id == behavior.Id), processId, targetWit.WorkItemType.ReferenceName); } #region Sync Pages ... // Making sure the pages themselves are in sync is not too hard.. let's do them first foreach (var page in sourceWit.Layout.Pages) { var targetPage = targetWit.Layout.Pages .FirstOrDefault(x => x.Label.Equals(page.Label, StringComparison.OrdinalIgnoreCase)); await SyncDefinitionType <WorkItemPage>( TargetModel.WorkItemPages, page, targetPage, processId, targetWit.WorkItemType.ReferenceName); } #endregion #region Sync Sections and Groups ... // Now that we know all the target pages are present we can iterate over what resides on pages.. // A page's constituent parts is a little more difficult because you need to support an existing // group or control that has moved pages, sections or groups foreach (var sourcePage in sourceWit.Layout.Pages) { var targetPage = targetWit.Layout.Pages .FirstOrDefault(x => x.Label.Equals(sourcePage.Label, StringComparison.OrdinalIgnoreCase)); foreach (var sourceSection in sourcePage.Sections) { foreach (var sourceGroup in sourceSection.Groups) { var sourceGroupKey = $"{sourceWit.WorkItemType.ReferenceName}::{sourcePage.Label}::{sourceSection.Id}::{sourceGroup.Label}"; // first let's see if the target has any inherited group for this source group .. // It will have a group.inherits != null/"" if it is inherited.. you can edit "system" groups if (!sourceGroup.Inherited) // It's a custom group { // look for the group on the flattened set of groups.. remember flat groups are keyed on $"{wit.ReferenceName}::{page.Label}::{section.Id}::{group.Label}" var existingGroup = TargetModel.WorkItemGroups.Select(x => new { x.Key, x.Value }) .FirstOrDefault(x => x.Key.StartsWith($"{targetWit.WorkItemType.ReferenceName}::") && x.Value.Label.Equals(sourceGroup.Label, StringComparison.OrdinalIgnoreCase)); if (existingGroup != null) { WorkItemGroup finalTargetGroup = null; WorkItemPage finalTargetPage = null; // here we know the group exists.. we need to check if its on the same page if (sourceGroupKey.Equals(existingGroup.Key, StringComparison.OrdinalIgnoreCase)) { // It's on the same page/section.. no need to move Log.LogInformation("Target group [{0}:{1}] located on same page/section. Skipping group location sync..", targetPage.Label, existingGroup.Value.Label); finalTargetPage = targetPage; finalTargetGroup = existingGroup.Value; } else { // It's on a different page or section.. we need to move it .. but how can we tell // if it moved to a different page or just a different section? We split and compare var sourceSplit = sourceGroupKey.Split("::".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var existingSplit = existingGroup.Key.Split("::".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var existingPage = targetWit.Layout.Pages.FirstOrDefault(p => p.Label.Equals(existingSplit[1])); if (sourceSplit[1].Equals(existingSplit[1], StringComparison.OrdinalIgnoreCase)) { // Its on the same page.. it must be section change.. lets move it to the new section var tempTargetGroup = existingGroup.Value.CloneAsNew(); tempTargetGroup.Id = existingGroup.Value.Id; if (await Target.MoveWorkItemGroupWithinPage( tempTargetGroup, processId, sourceWit.WorkItemType.ReferenceName, targetPage.Id, sourceSplit[2], existingSplit[2])) { Log.LogInformation("Target group [{0}] located on same page but different section. Moved from [{1}] to [{2}] ..", sourceGroup.Label, sourceSplit[2], existingSplit[2]); } else { Log.LogError("Target group [{0}] located on same page but different section. Unable to move from [{1}] to [{2}] ..", sourceGroup.Label, sourceSplit[2], existingSplit[2]); } finalTargetPage = existingPage; finalTargetGroup = tempTargetGroup; } else { // Its on a different page .. lets move pages var tempTargetGroup = existingGroup.Value.CloneAsNew(); tempTargetGroup.Id = existingGroup.Value.Id; if (await Target.MoveWorkItemGroupToNewPage( tempTargetGroup, processId, targetWit.WorkItemType.ReferenceName, targetPage.Id, sourceSplit[2], existingPage.Id, existingSplit[2])) { Log.LogInformation("Target group located on different page. Moved from [{0}:{1}] to [{2}:{3}] ..", sourceSplit[1], sourceSplit[2], existingSplit[1], existingSplit[2]); } else { Log.LogError("Target group located on different page. Unable to move from [{0}:{1}] to [{2}:{3}]!", existingSplit[1], existingSplit[2], targetPage.Label, sourceSplit[2]); } finalTargetPage = existingPage; finalTargetGroup = tempTargetGroup; } } // TODO Finish this! // Iterate through the source controls and make sure the target has all the controls from source foreach (var sourceControl in sourceGroup.Controls) { if (sourceControl.ControlType == "HtmlFieldControl") { Log.LogWarning("Skipped HTML control sync [{0}] as it should have already been migrated as part of the group sync.", sourceControl.Label); } else { // Let's see if we can't find the control already present in the "final target" var targetControl = finalTargetGroup.Controls.FirstOrDefault(ctl => ctl.Id.Equals(sourceControl.Id, StringComparison.OrdinalIgnoreCase)); if (targetControl == null) { // Let's see if its in another group perhaps.. if so we might want to move it.. that would imply that the group it was in is no longer WorkItemGroup oldGroup = null; foreach (var tempSection in finalTargetPage.Sections) { oldGroup = tempSection.Groups.FirstOrDefault(g => g.Controls.Any(c => c.Id.Equals(sourceControl.Id, StringComparison.OrdinalIgnoreCase))); if (oldGroup != null) { break; } } if (oldGroup == null) // It must be a new control { if (await Target.AddWorkItemControlToGroup(sourceControl.CloneAsNew(), processId, sourceWit.WorkItemType.ReferenceName, finalTargetGroup.Id, sourceControl.Id)) { Log.LogInformation("Attached control [{0}] to group [{1}].", sourceControl.Label, finalTargetGroup.Label); } else { Log.LogError("Failed to attach control [{0}] to new group [{1}]!", sourceControl.Label, finalTargetGroup.Label); } } else { // It must be control movement between groups if (await Target.MoveWorkItemControlToOtherGroup(sourceControl.CloneAsNew(), processId, sourceWit.WorkItemType.ReferenceName, finalTargetGroup.Id, sourceControl.Id, oldGroup.Id)) { Log.LogInformation("Moved control [{0}] from [{1}] to existing group [{2}].", sourceControl.Id, oldGroup.Label, finalTargetGroup.Label); } else { Log.LogError("Failed to move control [{0}] from [{1}] to existing group [{2}].", sourceControl.Id, oldGroup.Label, finalTargetGroup.Label); } } } else { { Log.LogInformation("Target already contains control [{0}] in proper group [{1}].", sourceControl.Label, finalTargetGroup.Label); } } } } } else { // Target doesn't have the group at all WorkItemGroup newGroup = await SyncDefinitionType <WorkItemGroup>(TargetModel.WorkItemGroups, sourceGroup, null, processId, sourceWit.WorkItemType.ReferenceName, targetPage.Id, sourceSection.Id); // Add all the controls foreach (var sourceControl in sourceGroup.Controls.Where(c => !c.ControlType.Equals("HtmlFieldControl", StringComparison.OrdinalIgnoreCase))) { if (await Target.AddWorkItemControlToGroup(sourceControl.CloneAsNew(), processId, sourceWit.WorkItemType.ReferenceName, newGroup.Id, sourceControl.Id)) { Log.LogInformation("Attached control [{0}] to new group [{1}].", sourceControl.Label, newGroup.Label); } else { Log.LogError("Failed to attach control [{0}] to new group [{1}]!", sourceControl.Label, newGroup.Label); } } } } } } } #endregion #region Sync Controls ... // Let's get a fresh layout from the target, now that we know pages and groups are aligned. await LoadLayout(TargetModel, targetWit, Target, processId); // At this point all Pages, Sections and Groups should be aligned.. lets sync the controls foreach (var sourcePage in sourceWit.Layout.Pages) { foreach (var sourceSection in sourcePage.Sections) { foreach (var sourceGroup in sourceSection.Groups) { foreach (var sourceControl in sourceGroup.Controls) { } } } } #endregion Log.LogInformation($"Completed sync of work item type [{Source.Options.Name}::{sourceWit.WorkItemType.Name}] in [{Target.Options.Name}::{targetWit.WorkItemType.Name}]."); }