feat: 增加父 PBI 列表加载功能与界面优化
- 更新 README.md,明确父 PBI Id 的选择方式,增加加载 PBI 列表的功能,提升用户在导入时的便利性。 - 在 MainForm.cs 中添加加载 PBI 列表的按钮,优化父项选择逻辑,确保用户能够方便地选择合适的父项。 - 修改 MainFormUi.cs,将父 PBI Id 输入框替换为下拉框,支持从列表中选择,提升用户体验。 - 在 TfsClient.cs 中实现获取父项候选的逻辑,确保用户能够根据项目和迭代路径获取相关的工作项信息。 - 调整界面布局,确保父 PBI 选择与其他输入项一致,提升整体视觉效果。
This commit is contained in:
+126
-23
@@ -38,17 +38,25 @@ namespace TfsExcelImporter
|
||||
_btnTest.Click += async (s, e) => await TestConnectionAsync();
|
||||
_btnLoadIterations.Click += async (s, e) => await LoadIterationsAsync();
|
||||
_btnLoadAreas.Click += async (s, e) => await LoadAreasAsync();
|
||||
_btnLoadPbi.Click += async (s, e) => await LoadBacklogAsync();
|
||||
_btnTemplate.Click += (s, e) => ExportTemplate();
|
||||
_btnPreview.Click += async (s, e) => await PreviewDataAsync();
|
||||
_btnImport.Click += async (s, e) => await ImportAsync();
|
||||
_btnFixArea.Click += async (s, e) => await FixAreaAsync();
|
||||
_btnLinkParent.Click += async (s, e) => await LinkParentAsync();
|
||||
|
||||
foreach (var tb in new[] { _txtUrl, _txtProject, _txtType, _txtUser, _txtPwd,
|
||||
_txtExcel, _txtParentPbi })
|
||||
foreach (var tb in new[] { _txtUrl, _txtProject, _txtType, _txtUser, _txtPwd, _txtExcel })
|
||||
tb.TextChanged += (s, e) => OnFieldChanged();
|
||||
_cmbIteration.TextChanged += (s, e) => OnFieldChanged();
|
||||
_cmbArea.TextChanged += (s, e) => OnFieldChanged();
|
||||
_cmbParent.TextChanged += (s, e) => OnFieldChanged();
|
||||
// 从列表里挑的是「#219975 标题 · 状态」这种展示文本,落库/导入得是纯 Id。
|
||||
// 两个事件都挂:鼠标点选主要走 SelectionChangeCommitted(下拉关闭时),
|
||||
// 键盘上下选择/程序置位只有 SelectedIndexChanged,重复选同一项则只有前者。
|
||||
_cmbParent.SelectedIndexChanged += (s, e) => CommitPbiSelection();
|
||||
_cmbParent.SelectionChangeCommitted += (s, e) => CommitPbiSelection();
|
||||
// 下拉收起时再兜一次:无论键盘还是鼠标选完,这里一定能拿到最终选中项
|
||||
_cmbParent.DropDownClosed += (s, e) => CommitPbiSelection();
|
||||
_chkSkip.CheckedChanged += (s, e) => OnFieldChanged();
|
||||
|
||||
FormClosing += (s, e) => { try { SaveUiToConfig(); } catch { } };
|
||||
@@ -72,7 +80,7 @@ namespace TfsExcelImporter
|
||||
_txtExcel.Text = _cfg.LastExcelPath;
|
||||
_cmbArea.Text = _cfg.AreaPath;
|
||||
_chkSkip.Checked = _cfg.SkipExistingTitles;
|
||||
_txtParentPbi.Text = _cfg.ParentPbiId;
|
||||
_cmbParent.Text = _cfg.ParentPbiId;
|
||||
}
|
||||
|
||||
private void SaveUiToConfig()
|
||||
@@ -86,7 +94,7 @@ namespace TfsExcelImporter
|
||||
_cfg.LastExcelPath = _txtExcel.Text.Trim();
|
||||
_cfg.AreaPath = _cmbArea.Text.Trim();
|
||||
_cfg.SkipExistingTitles = _chkSkip.Checked;
|
||||
_cfg.ParentPbiId = _txtParentPbi.Text.Trim();
|
||||
_cfg.ParentPbiId = NormalizePbiText(_cmbParent.Text);
|
||||
_cfg.Save();
|
||||
}
|
||||
|
||||
@@ -209,24 +217,7 @@ namespace TfsExcelImporter
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = FirstLine(ex.Message);
|
||||
// TF30063 = 服务器拒绝当前身份:与「连不上」是两回事,提示要能让用户知道该去填账号密码
|
||||
if (msg.IndexOf("TF30063", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
SetConnection("认证失败", ChipKind.Danger);
|
||||
SetBadgeOf(_cardConn, BadgeKind.Warn);
|
||||
ShowError(err, field, _cardData, "认证失败:请在步骤 1 填写可访问该项目的用户名/密码");
|
||||
Log($"加载 {what} 失败: 未授权(TF30063),当前 Windows 身份无权访问该项目");
|
||||
Log("排查: 用户名可写 user 或 DOMAIN\\user;程序固定走 NTLM 直连。");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetConnection("连接失败", ChipKind.Danger);
|
||||
SetBadgeOf(_cardConn, BadgeKind.Warn);
|
||||
ShowError(err, field, _cardData, $"{what} 列表加载失败,可手填完整路径");
|
||||
Log($"加载 {what} 失败: " + msg);
|
||||
Log("排查: 程序固定直连(不走本机系统代理);DNS 失败多为 VPN 或未连内网。");
|
||||
}
|
||||
ReportTfsFailure(ex, what, field, err);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -235,6 +226,109 @@ namespace TfsExcelImporter
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- 父 PBI 列表
|
||||
private async Task LoadBacklogAsync()
|
||||
{
|
||||
SaveUiToConfig();
|
||||
if (string.IsNullOrWhiteSpace(_cfg.TfsUrl))
|
||||
{
|
||||
Append("请先填写服务器 URL", LogLevel.Warn);
|
||||
OpenDetail(_cardConn);
|
||||
return;
|
||||
}
|
||||
SetBusy(true);
|
||||
SetConnection("读取 PBI 列表中…", ChipKind.Info);
|
||||
try
|
||||
{
|
||||
using (var client = new TfsClient(_cfg))
|
||||
{
|
||||
Log("测试连接...");
|
||||
Log(await client.TestConnectionAsync());
|
||||
var items = await client.GetBacklogItemsAsync(_cfg.Project, _cfg.IterationPath, Log);
|
||||
// 下拉项是「#219975 标题 · 状态」,但字段本身只存纯 Id:
|
||||
// Items.Clear 会连带清空 Text,所以先存后回填,刷新列表不丢已填的 Id。
|
||||
var keep = _cmbParent.Text;
|
||||
_cmbParent.Items.Clear();
|
||||
foreach (var it in items) _cmbParent.Items.Add(it);
|
||||
_cmbParent.Text = keep;
|
||||
Log($"已加载 {items.Count} 条候选父项(从列表选中后会自动回填 Id)");
|
||||
if (items.Count == 0) Log("提示: 没有可选父项,可直接手填 Id,或先确认项目/迭代是否正确");
|
||||
SetConnection("已连接", ChipKind.Ok);
|
||||
SetBadgeOf(_cardConn, BadgeKind.Done);
|
||||
ShowError(_errParent, _fParent, _cardData, "");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReportTfsFailure(ex, "父 PBI", _fParent, _errParent);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetBusy(false);
|
||||
OnFieldChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 列表项是「#219975 标题 · State」这种展示文本,字段要的是纯数字 Id:
|
||||
/// 选中后立刻把 Text 归位成 Id,避免展示串被写进 System.Parent 或落进 config。
|
||||
/// 用 SelectedIndexChanged 而不是 SelectionChangeCommitted——后者只在「下拉关闭」时才发,
|
||||
/// 键盘/程序触发的选中收不到;这里以「# 开头」判定是不是列表回填,手输纯 Id 不受影响。
|
||||
/// 回填 Text 会让 SelectedIndex 变 -1 再触发一次,此时文本已是数字,直接 return 不会递归。
|
||||
/// </summary>
|
||||
private void CommitPbiSelection()
|
||||
{
|
||||
int idx = _cmbParent.SelectedIndex;
|
||||
if (idx < 0 || idx >= _cmbParent.Items.Count) return; // 回填后 SelectedIndex 会变 -1,这条路用于收敛
|
||||
var item = _cmbParent.Items[idx] as TfsClient.WorkItemBrief;
|
||||
if (item == null) return;
|
||||
var id = item.Id.ToString();
|
||||
if (string.Equals(_cmbParent.Text, id, StringComparison.Ordinal)) return;
|
||||
_cmbParent.Text = id;
|
||||
_cmbParent.SelectionStart = id.Length;
|
||||
Log($"父 PBI 已选: #{item.Id} {item.Title}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把「#219975 标题 · State」洗成纯 Id 再落库。
|
||||
/// 选中事件负责即时回填,这里是最后一道保险:事件万一没触发(比如某些自绘交互),
|
||||
/// 写进 config / 传给 TFS 的仍是干净 Id,不会出现「挂不上父项却看不出原因」。
|
||||
/// </summary>
|
||||
private static string NormalizePbiText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return "";
|
||||
text = text.Trim();
|
||||
if (text[0] != '#') return text;
|
||||
int i = 1;
|
||||
while (i < text.Length && text[i] >= '0' && text[i] <= '9') i++;
|
||||
return i > 1 ? text.Substring(1, i - 1) : text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接类失败的统一呈现:TF30063(服务器拒绝当前身份)与网络不通是两回事,
|
||||
/// 前者该去填账号密码,后者才查 VPN/代理,提示分开写才能真正指引到动作。
|
||||
/// </summary>
|
||||
private void ReportTfsFailure(Exception ex, string what, InputField field, Label err)
|
||||
{
|
||||
var msg = FirstLine(ex.Message);
|
||||
if (msg.IndexOf("TF30063", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
SetConnection("认证失败", ChipKind.Danger);
|
||||
SetBadgeOf(_cardConn, BadgeKind.Warn);
|
||||
ShowError(err, field, _cardData, "认证失败:请在步骤 1 填写可访问该项目的用户名/密码");
|
||||
Log($"加载 {what} 失败: 未授权(TF30063),当前 Windows 身份无权访问该项目");
|
||||
Log("排查: 用户名可写 user 或 DOMAIN\\user;程序固定走 NTLM 直连。");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetConnection("连接失败", ChipKind.Danger);
|
||||
SetBadgeOf(_cardConn, BadgeKind.Warn);
|
||||
ShowError(err, field, _cardData, $"{what} 列表加载失败,可手动填写");
|
||||
Log($"加载 {what} 失败: " + msg);
|
||||
Log("排查: 程序固定直连(不走本机系统代理);DNS 失败多为 VPN 或未连内网。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 填充路径下拉:先保留当前手填/已选内容,刷新列表不冲掉用户的选择。
|
||||
/// Items.Clear 会把 DropDown 样式的 Text 一并清空,所以要在清空前取值、匹配后回填。
|
||||
@@ -531,7 +625,7 @@ namespace TfsExcelImporter
|
||||
if (!int.TryParse(_cfg.ParentPbiId, out parentId))
|
||||
{
|
||||
ShowError(_errParent, _fParent, _cardData, "父 PBI Id 需为纯数字,例如 219975");
|
||||
_txtParentPbi.Focus();
|
||||
_cmbParent.Focus();
|
||||
return;
|
||||
}
|
||||
var confirm = MessageBox.Show(this,
|
||||
@@ -583,6 +677,15 @@ namespace TfsExcelImporter
|
||||
_cmbArea.Focus();
|
||||
return;
|
||||
}
|
||||
// 父 PBI 是可选字段,但一旦填了就必须是纯数字:非数字会在挂接时被静默当成「不挂父项」,
|
||||
// 导入"成功"却少了层级,比直接报错更难发现。
|
||||
if (!string.IsNullOrWhiteSpace(_cfg.ParentPbiId) && !int.TryParse(_cfg.ParentPbiId, out _))
|
||||
{
|
||||
ShowError(_errParent, _fParent, _cardData, "父 PBI Id 需为纯数字,可点右侧「加载 PBI 列表」选择");
|
||||
Append("导入取消:父 PBI Id 不是数字", LogLevel.Warn);
|
||||
_cmbParent.Focus();
|
||||
return;
|
||||
}
|
||||
var path = _txtExcel.Text.Trim();
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user