1147 lines
47 KiB
C#
1147 lines
47 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Project.Tasks.Editor
|
|
{
|
|
internal sealed class TaskBoardWindow : EditorWindow
|
|
{
|
|
private const float ColumnWidth = 290f;
|
|
private const float DefaultDetailsWidth = 420f;
|
|
private const float MinDetailsWidth = 320f;
|
|
private const float MinBoardWidth = 420f;
|
|
private const float SplitterWidth = 5f;
|
|
private const float MinButtonSize = 9f;
|
|
private const string CustomOwnerOption = "<Custom>";
|
|
private const string DetailsWidthPrefsKey = "Project.Tasks.Editor.TaskBoardWindow.DetailsWidth";
|
|
private const double DeleteConfirmTimeoutSeconds = 3.0;
|
|
|
|
private TaskBoardData data;
|
|
private Vector2 boardScroll;
|
|
private Vector2 detailsScroll;
|
|
private string searchText = string.Empty;
|
|
private string sortOption = "Priority Desc";
|
|
private bool filtersExpanded = true;
|
|
|
|
private readonly HashSet<string> selectedStatuses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> selectedPriorities = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> selectedAreas = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> selectedOwners = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> selectedFileStates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
private readonly HashSet<string> selectedWarningStates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private TaskRecord selectedTask;
|
|
private string saveMessage = string.Empty;
|
|
private MessageType saveMessageType = MessageType.Info;
|
|
|
|
private string editStatus;
|
|
private string editPriority;
|
|
private string editOwner;
|
|
private bool editOwnerIsCustom;
|
|
private string editExecutionTime;
|
|
private string editIndexSummary;
|
|
private string editTaskSummary;
|
|
private string editWhy;
|
|
private string editExpectedOutcome;
|
|
private string editCurrentContext;
|
|
private string editAcceptanceCriteria;
|
|
private string editVerification;
|
|
private string editRisks;
|
|
private string editHumanDecisions;
|
|
private string editDecisionLog;
|
|
private string editHandoffNotes;
|
|
|
|
private GUIStyle readOnlyWrappedTextArea;
|
|
private GUIStyle priorityBadgeStyle;
|
|
private GUIStyle compactButtonStyle;
|
|
private TaskRecord pressedTask;
|
|
private Vector2 pressedMousePosition;
|
|
private float detailsWidth = DefaultDetailsWidth;
|
|
private bool isResizingDetails;
|
|
private string pendingDeleteTaskId;
|
|
private double pendingDeleteUntil;
|
|
|
|
[MenuItem("Tools/Tasks/Kanban Board")]
|
|
public static void Open()
|
|
{
|
|
GetWindow<TaskBoardWindow>("Task Board");
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
detailsWidth = EditorPrefs.GetFloat(DetailsWidthPrefsKey, DefaultDetailsWidth);
|
|
Reload();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
EditorPrefs.SetFloat(DetailsWidthPrefsKey, detailsWidth);
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
ResetDeleteConfirmationIfExpired();
|
|
DrawToolbar();
|
|
|
|
if (data == null)
|
|
{
|
|
EditorGUILayout.HelpBox("Данные задач еще не загружены.", MessageType.Info);
|
|
return;
|
|
}
|
|
|
|
if (data.Warnings.Count > 0)
|
|
{
|
|
EditorGUILayout.HelpBox(string.Join("\n", data.Warnings), MessageType.Warning);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(saveMessage))
|
|
{
|
|
EditorGUILayout.HelpBox(saveMessage, saveMessageType);
|
|
}
|
|
|
|
DrawSummaryBar();
|
|
DrawFilterPanel();
|
|
|
|
float clampedDetailsWidth = Mathf.Clamp(detailsWidth, MinDetailsWidth, Mathf.Max(MinDetailsWidth, position.width - MinBoardWidth));
|
|
if (!Mathf.Approximately(clampedDetailsWidth, detailsWidth))
|
|
{
|
|
detailsWidth = clampedDetailsWidth;
|
|
}
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
DrawBoard(position.width - detailsWidth - SplitterWidth - 24f);
|
|
DrawDetailsSplitter();
|
|
DrawDetails(detailsWidth);
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void DrawToolbar()
|
|
{
|
|
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
|
|
|
if (GUILayout.Button("Reload", EditorStyles.toolbarButton))
|
|
{
|
|
Reload(selectedTask != null ? selectedTask.Id : null);
|
|
}
|
|
|
|
if (GUILayout.Button("Index", EditorStyles.toolbarButton))
|
|
{
|
|
string indexPath = data != null ? data.IndexPath : TaskBoardService.NormalizePath(System.IO.Path.Combine(TaskBoardService.GetProjectRoot(), "docs", "tasks", "Index.md"));
|
|
TaskBoardService.OpenInDefaultApp(indexPath);
|
|
}
|
|
|
|
if (GUILayout.Button("Owners", EditorStyles.toolbarButton))
|
|
{
|
|
OpenOwnersConfig();
|
|
}
|
|
|
|
GUILayout.Space(8f);
|
|
GUILayout.Label("Search", GUILayout.Width(45f));
|
|
searchText = GUILayout.TextField(searchText, GUI.skin.FindStyle("ToolbarSeachTextField") ?? EditorStyles.toolbarTextField, GUILayout.MinWidth(180f));
|
|
|
|
GUILayout.FlexibleSpace();
|
|
EditorGUILayout.EndHorizontal();
|
|
}
|
|
|
|
private void DrawSummaryBar()
|
|
{
|
|
if (data == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
List<TaskRecord> filteredTasks = GetFilteredTasks();
|
|
int totalMinutes = TaskBoardService.SumEstimatedMinutes(filteredTasks);
|
|
int warnings = filteredTasks.Count(task => task.ValidationMessages.Count > 0);
|
|
int missingFiles = filteredTasks.Count(task => !task.FileExists);
|
|
|
|
EditorGUILayout.BeginVertical("box");
|
|
EditorGUILayout.LabelField(
|
|
"Visible Tasks: " + filteredTasks.Count + " / " + data.Tasks.Count
|
|
+ " | Visible Time: " + TaskBoardService.FormatMinutesForDisplay(totalMinutes)
|
|
+ " | Warnings: " + warnings
|
|
+ " | Missing Files: " + missingFiles,
|
|
EditorStyles.wordWrappedMiniLabel);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
GUILayout.Label("Sort", GUILayout.Width(32f));
|
|
sortOption = DrawPopup(sortOption, BuildSortOptions(), 160f);
|
|
GUILayout.FlexibleSpace();
|
|
EditorGUILayout.EndHorizontal();
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
private void DrawFilterPanel()
|
|
{
|
|
EditorGUILayout.BeginVertical("box");
|
|
filtersExpanded = EditorGUILayout.Foldout(filtersExpanded, "Filters", true);
|
|
if (filtersExpanded)
|
|
{
|
|
DrawSelectionGroup("Statuses", TaskBoardConstants.Statuses, selectedStatuses);
|
|
DrawSelectionGroup("Priorities", TaskBoardConstants.Priorities, selectedPriorities);
|
|
DrawSelectionGroup("Areas", BuildAreaOptions(), selectedAreas);
|
|
DrawSelectionGroup("Owners", BuildOwnerFilterOptions(), selectedOwners);
|
|
DrawSelectionGroup("Files", new[] { "Existing", "Missing" }, selectedFileStates);
|
|
DrawSelectionGroup("Warnings", new[] { "Warnings", "Clean" }, selectedWarningStates);
|
|
}
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
private void DrawSelectionGroup(string label, string[] options, HashSet<string> selectedValues)
|
|
{
|
|
if (options == null || options.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GUIStyle buttonStyle = GetCompactButtonStyle();
|
|
|
|
EditorGUILayout.BeginVertical("box");
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(label, EditorStyles.boldLabel);
|
|
if (GUILayout.Button("All", buttonStyle))
|
|
{
|
|
SetAll(selectedValues, options, true);
|
|
}
|
|
|
|
if (GUILayout.Button("Nothing", buttonStyle))
|
|
{
|
|
selectedValues.Clear();
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
for (int i = 0; i < options.Length; i++)
|
|
{
|
|
string option = options[i];
|
|
bool current = selectedValues.Contains(option);
|
|
bool next = GUILayout.Toggle(current, option, buttonStyle);
|
|
if (next != current)
|
|
{
|
|
if (next)
|
|
{
|
|
selectedValues.Add(option);
|
|
}
|
|
else
|
|
{
|
|
selectedValues.Remove(option);
|
|
}
|
|
}
|
|
|
|
if ((i + 1) % 4 == 0)
|
|
{
|
|
EditorGUILayout.EndHorizontal();
|
|
EditorGUILayout.BeginHorizontal();
|
|
}
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
private void DrawBoard(float width)
|
|
{
|
|
List<TaskRecord> filteredTasks = GetFilteredTasks();
|
|
|
|
EditorGUILayout.BeginVertical(GUILayout.Width(Mathf.Max(MinBoardWidth, width)), GUILayout.ExpandHeight(true));
|
|
boardScroll = EditorGUILayout.BeginScrollView(boardScroll, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
foreach (string status in TaskBoardConstants.Statuses)
|
|
{
|
|
DrawColumn(status, filteredTasks.Where(task => string.Equals(task.Status, status, StringComparison.OrdinalIgnoreCase)).ToList());
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
private void DrawColumn(string status, List<TaskRecord> tasks)
|
|
{
|
|
EditorGUILayout.BeginVertical("box", GUILayout.Width(ColumnWidth), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.LabelField(status, EditorStyles.boldLabel);
|
|
EditorGUILayout.LabelField(tasks.Count + " task(s)", EditorStyles.miniLabel);
|
|
EditorGUILayout.LabelField(TaskBoardService.FormatMinutesForDisplay(TaskBoardService.SumEstimatedMinutes(tasks)), EditorStyles.miniLabel);
|
|
EditorGUILayout.Space(6f);
|
|
|
|
Rect headerRect = GUILayoutUtility.GetLastRect();
|
|
Rect lastRect = headerRect;
|
|
|
|
if (tasks.Count == 0)
|
|
{
|
|
EditorGUILayout.HelpBox("Нет задач по текущему фильтру.", MessageType.None);
|
|
lastRect = GUILayoutUtility.GetLastRect();
|
|
}
|
|
|
|
foreach (TaskRecord task in tasks)
|
|
{
|
|
DrawTaskCard(task);
|
|
lastRect = GUILayoutUtility.GetLastRect();
|
|
EditorGUILayout.Space(6f);
|
|
}
|
|
|
|
GUILayout.FlexibleSpace();
|
|
EditorGUILayout.EndVertical();
|
|
|
|
Rect columnRect = new Rect(headerRect.xMin - 6f, headerRect.yMin - 28f, ColumnWidth, Mathf.Max(110f, lastRect.yMax - headerRect.yMin + 36f));
|
|
HandleColumnDrop(columnRect, status);
|
|
}
|
|
|
|
private void DrawTaskCard(TaskRecord task)
|
|
{
|
|
Color previousBackground = GUI.backgroundColor;
|
|
GUI.backgroundColor = selectedTask == task ? new Color(0.72f, 0.82f, 1f) : GetStatusColor(task.Status);
|
|
|
|
EditorGUILayout.BeginVertical("box");
|
|
GUI.backgroundColor = previousBackground;
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField(task.Id, EditorStyles.boldLabel);
|
|
GUILayout.FlexibleSpace();
|
|
DrawPriorityBadge(task.Priority);
|
|
EditorGUILayout.EndHorizontal();
|
|
Rect firstRect = GUILayoutUtility.GetLastRect();
|
|
|
|
EditorGUILayout.LabelField(string.IsNullOrEmpty(task.Title) ? task.Id : task.Title, EditorStyles.wordWrappedLabel);
|
|
if (!string.IsNullOrEmpty(task.IndexSummary))
|
|
{
|
|
EditorGUILayout.LabelField(task.IndexSummary, EditorStyles.wordWrappedMiniLabel);
|
|
}
|
|
|
|
EditorGUILayout.Space(4f);
|
|
EditorGUILayout.LabelField("Area: " + SafeValue(task.Area) + " Owner: " + SafeValue(task.Owner), EditorStyles.miniLabel);
|
|
EditorGUILayout.LabelField("Time: " + GetFormattedTime(task), EditorStyles.miniLabel);
|
|
|
|
if (task.ValidationMessages.Count > 0)
|
|
{
|
|
EditorGUILayout.HelpBox(task.ValidationMessages[0], MessageType.Warning);
|
|
}
|
|
|
|
if (GUILayout.Button("Details", GetCompactButtonStyle()))
|
|
{
|
|
SelectTask(task);
|
|
}
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
Rect lastRect = GUILayoutUtility.GetLastRect();
|
|
Rect cardRect = new Rect(firstRect.xMin - 6f, firstRect.yMin - 6f, firstRect.width + 12f, Mathf.Max(44f, lastRect.yMax - firstRect.yMin + 12f));
|
|
HandleCardDrag(task, cardRect);
|
|
}
|
|
|
|
private void DrawDetails(float width)
|
|
{
|
|
EditorGUILayout.BeginVertical("box", GUILayout.Width(width), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.LabelField("Task Details", EditorStyles.boldLabel);
|
|
|
|
if (selectedTask == null)
|
|
{
|
|
EditorGUILayout.HelpBox("Выберите карточку, чтобы посмотреть детали задачи и изменить статус.", MessageType.Info);
|
|
EditorGUILayout.EndVertical();
|
|
return;
|
|
}
|
|
|
|
detailsScroll = EditorGUILayout.BeginScrollView(detailsScroll, GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.LabelField(selectedTask.Id, EditorStyles.boldLabel);
|
|
EditorGUILayout.LabelField(string.IsNullOrEmpty(selectedTask.Title) ? selectedTask.Id : selectedTask.Title, EditorStyles.wordWrappedLabel);
|
|
EditorGUILayout.Space(8f);
|
|
|
|
if (selectedTask.ValidationMessages.Count > 0)
|
|
{
|
|
EditorGUILayout.HelpBox(string.Join("\n", selectedTask.ValidationMessages), MessageType.Warning);
|
|
}
|
|
|
|
if (!selectedTask.FileExists)
|
|
{
|
|
EditorGUILayout.HelpBox("Task-файл отсутствует. Поля из task-файла недоступны до создания файла по шаблону.", MessageType.Info);
|
|
}
|
|
|
|
GUIStyle btn = GetCompactButtonStyle();
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
using (new EditorGUI.DisabledScope(!selectedTask.FileExists))
|
|
{
|
|
if (GUILayout.Button("File", btn, GUILayout.Width(75f), GUILayout.Height(18f)))
|
|
{
|
|
TaskBoardService.OpenInDefaultApp(selectedTask.AbsoluteFilePath);
|
|
}
|
|
}
|
|
|
|
using (new EditorGUI.DisabledScope(selectedTask.FileExists))
|
|
{
|
|
if (GUILayout.Button("Create", btn, GUILayout.Width(75f), GUILayout.Height(18f)))
|
|
{
|
|
CreateSelectedTaskFromTemplate();
|
|
}
|
|
}
|
|
|
|
if (GUILayout.Button("Index", btn, GUILayout.Width(75f), GUILayout.Height(18f)))
|
|
{
|
|
TaskBoardService.OpenInDefaultApp(data.IndexPath);
|
|
}
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUILayout.Space(4f);
|
|
DrawDeleteButton();
|
|
|
|
EditorGUILayout.Space(8f);
|
|
DrawEditableFields();
|
|
EditorGUILayout.Space(12f);
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
EditorGUILayout.LabelField("Priority", GUILayout.Width(80f));
|
|
DrawPriorityBadge(selectedTask.Priority);
|
|
EditorGUILayout.EndHorizontal();
|
|
DrawReadOnlyField("Estimate", GetFormattedTime(selectedTask));
|
|
DrawReadOnlyField("File", selectedTask.RelativeFilePath);
|
|
DrawReadOnlyField("Created", selectedTask.Created);
|
|
DrawReadOnlyField("Updated", selectedTask.Updated);
|
|
DrawReadOnlyField("Area", selectedTask.Area);
|
|
|
|
EditorGUILayout.Space(10f);
|
|
EditorGUILayout.LabelField("Task File Sections", EditorStyles.boldLabel);
|
|
DrawEditableSection("Why", ref editWhy, selectedTask.FileExists);
|
|
DrawEditableSection("Expected Outcome", ref editExpectedOutcome, selectedTask.FileExists);
|
|
DrawEditableSection("Current Context", ref editCurrentContext, selectedTask.FileExists);
|
|
DrawEditableSection("Acceptance Criteria", ref editAcceptanceCriteria, selectedTask.FileExists);
|
|
DrawEditableSection("Verification", ref editVerification, selectedTask.FileExists);
|
|
DrawEditableSection("Risks / Open Questions", ref editRisks, selectedTask.FileExists);
|
|
DrawEditableSection("Human Decisions Needed", ref editHumanDecisions, selectedTask.FileExists);
|
|
DrawEditableSection("Decision Log", ref editDecisionLog, selectedTask.FileExists);
|
|
DrawEditableSection("Handoff Notes", ref editHandoffNotes, selectedTask.FileExists);
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
|
|
private void DrawEditableFields()
|
|
{
|
|
EditorGUILayout.LabelField("Index Fields", EditorStyles.boldLabel);
|
|
editStatus = DrawStringPopup("Status", editStatus, TaskBoardConstants.Statuses);
|
|
editPriority = DrawStringPopup("Priority", editPriority, TaskBoardConstants.Priorities);
|
|
DrawOwnerField();
|
|
editExecutionTime = EditorGUILayout.TextField("Execution Time", editExecutionTime ?? string.Empty);
|
|
|
|
string normalizedExecutionTime;
|
|
int estimatedMinutes;
|
|
if (TaskBoardService.TryNormalizeExecutionTime(editExecutionTime, out normalizedExecutionTime, out estimatedMinutes))
|
|
{
|
|
editExecutionTime = normalizedExecutionTime;
|
|
}
|
|
|
|
EditorGUILayout.LabelField("Estimate", TaskBoardService.IsValidExecutionTime(editExecutionTime)
|
|
? TaskBoardService.FormatExecutionTimeForDisplay(editExecutionTime)
|
|
: "-");
|
|
|
|
EditorGUILayout.LabelField("Index Summary");
|
|
editIndexSummary = EditorGUILayout.TextArea(editIndexSummary ?? string.Empty, GUILayout.MinHeight(60f));
|
|
|
|
EditorGUILayout.Space(8f);
|
|
EditorGUILayout.LabelField("Task File Fields", EditorStyles.boldLabel);
|
|
using (new EditorGUI.DisabledScope(!selectedTask.FileExists))
|
|
{
|
|
EditorGUILayout.LabelField("Task Summary");
|
|
editTaskSummary = EditorGUILayout.TextArea(editTaskSummary ?? string.Empty, GUILayout.MinHeight(60f));
|
|
}
|
|
|
|
bool hasChanges = HasChanges();
|
|
bool executionTimeValid = TaskBoardService.IsValidExecutionTime(editExecutionTime);
|
|
if (!executionTimeValid)
|
|
{
|
|
EditorGUILayout.HelpBox("execution_time должен быть в формате Jira, например 1d6h30m, и быть кратным 30 минутам.", MessageType.Warning);
|
|
}
|
|
|
|
using (new EditorGUI.DisabledScope(!hasChanges || !executionTimeValid))
|
|
{
|
|
if (GUILayout.Button("Save", GetCompactButtonStyle()))
|
|
{
|
|
SaveSelectedTask();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawDeleteButton()
|
|
{
|
|
if (selectedTask == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isPending = string.Equals(pendingDeleteTaskId, selectedTask.Id, StringComparison.OrdinalIgnoreCase) && EditorApplication.timeSinceStartup <= pendingDeleteUntil;
|
|
Color previous = GUI.backgroundColor;
|
|
GUI.backgroundColor = isPending ? new Color(0.85f, 0.3f, 0.3f) : new Color(0.7f, 0.25f, 0.25f);
|
|
if (GUILayout.Button(isPending ? "Sure?" : "Delete Task", GetCompactButtonStyle(), GUILayout.Width(75f), GUILayout.Height(18f)))
|
|
{
|
|
if (isPending)
|
|
{
|
|
DeleteSelectedTask();
|
|
}
|
|
else
|
|
{
|
|
pendingDeleteTaskId = selectedTask.Id;
|
|
pendingDeleteUntil = EditorApplication.timeSinceStartup + DeleteConfirmTimeoutSeconds;
|
|
}
|
|
}
|
|
GUI.backgroundColor = previous;
|
|
}
|
|
|
|
private string DrawStringPopup(string label, string currentValue, string[] options)
|
|
{
|
|
int index = Array.IndexOf(options, currentValue);
|
|
if (index < 0)
|
|
{
|
|
index = 0;
|
|
}
|
|
|
|
int nextIndex = EditorGUILayout.Popup(label, index, options);
|
|
return options[Mathf.Clamp(nextIndex, 0, options.Length - 1)];
|
|
}
|
|
|
|
private string DrawPopup(string currentValue, string[] options, float width)
|
|
{
|
|
int index = Array.IndexOf(options, currentValue);
|
|
if (index < 0)
|
|
{
|
|
index = 0;
|
|
}
|
|
|
|
int nextIndex = EditorGUILayout.Popup(index, options, EditorStyles.toolbarPopup, GUILayout.Width(width));
|
|
return options[Mathf.Clamp(nextIndex, 0, options.Length - 1)];
|
|
}
|
|
|
|
private void DrawReadOnlyField(string label, string value)
|
|
{
|
|
EditorGUILayout.LabelField(label, string.IsNullOrEmpty(value) ? "-" : value);
|
|
}
|
|
|
|
private void DrawEditableSection(string title, ref string content, bool enabled)
|
|
{
|
|
EditorGUILayout.LabelField(title, EditorStyles.boldLabel);
|
|
using (new EditorGUI.DisabledScope(!enabled))
|
|
{
|
|
content = EditorGUILayout.TextArea(enabled ? (content ?? string.Empty) : string.Empty, GetReadOnlyWrappedTextArea(), GUILayout.MinHeight(52f));
|
|
}
|
|
EditorGUILayout.Space(6f);
|
|
}
|
|
|
|
private GUIStyle GetReadOnlyWrappedTextArea()
|
|
{
|
|
if (readOnlyWrappedTextArea == null)
|
|
{
|
|
readOnlyWrappedTextArea = new GUIStyle(EditorStyles.textArea) { wordWrap = true };
|
|
}
|
|
|
|
return readOnlyWrappedTextArea;
|
|
}
|
|
|
|
private GUIStyle GetPriorityBadgeStyle()
|
|
{
|
|
if (priorityBadgeStyle == null)
|
|
{
|
|
priorityBadgeStyle = new GUIStyle(EditorStyles.miniBoldLabel)
|
|
{
|
|
alignment = TextAnchor.MiddleCenter,
|
|
padding = new RectOffset(8, 8, 3, 3),
|
|
normal = { textColor = Color.white },
|
|
};
|
|
}
|
|
|
|
return priorityBadgeStyle;
|
|
}
|
|
|
|
private GUIStyle GetCompactButtonStyle()
|
|
{
|
|
if (compactButtonStyle == null)
|
|
{
|
|
compactButtonStyle = new GUIStyle("Button")
|
|
{
|
|
alignment = TextAnchor.MiddleCenter,
|
|
padding = new RectOffset(4, 4, 2, 2),
|
|
margin = new RectOffset(1, 1, 1, 1),
|
|
stretchWidth = true,
|
|
};
|
|
}
|
|
|
|
return compactButtonStyle;
|
|
}
|
|
|
|
private void DrawPriorityBadge(string priority)
|
|
{
|
|
GUIStyle style = GetPriorityBadgeStyle();
|
|
Vector2 contentSize = style.CalcSize(new GUIContent(priority));
|
|
Rect rect = GUILayoutUtility.GetRect(contentSize.x + 18f, 22f, GUILayout.Width(contentSize.x + 18f), GUILayout.Height(22f));
|
|
EditorGUI.DrawRect(rect, GetPriorityColor(priority));
|
|
GUI.Label(rect, priority, style);
|
|
}
|
|
|
|
private void DrawOwnerField()
|
|
{
|
|
string[] presetOptions = BuildOwnerPresetOptions();
|
|
int currentIndex = GetOwnerPopupIndex(presetOptions);
|
|
int nextIndex = EditorGUILayout.Popup("Owner", currentIndex, presetOptions);
|
|
|
|
if (nextIndex >= 0 && nextIndex < presetOptions.Length)
|
|
{
|
|
string selectedValue = presetOptions[nextIndex];
|
|
if (string.Equals(selectedValue, CustomOwnerOption, StringComparison.Ordinal))
|
|
{
|
|
editOwnerIsCustom = true;
|
|
}
|
|
else
|
|
{
|
|
editOwnerIsCustom = false;
|
|
editOwner = selectedValue;
|
|
}
|
|
}
|
|
|
|
if (editOwnerIsCustom)
|
|
{
|
|
editOwner = EditorGUILayout.TextField("Custom Owner", editOwner ?? string.Empty);
|
|
}
|
|
}
|
|
|
|
private void HandleCardDrag(TaskRecord task, Rect cardRect)
|
|
{
|
|
Event current = Event.current;
|
|
if (current.type == EventType.MouseDown && current.button == 0 && cardRect.Contains(current.mousePosition))
|
|
{
|
|
pressedTask = task;
|
|
pressedMousePosition = current.mousePosition;
|
|
}
|
|
|
|
if (pressedTask != task)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (current.type == EventType.MouseDrag && (current.mousePosition - pressedMousePosition).sqrMagnitude > 16f)
|
|
{
|
|
DragAndDrop.PrepareStartDrag();
|
|
DragAndDrop.objectReferences = Array.Empty<UnityEngine.Object>();
|
|
DragAndDrop.SetGenericData("TaskBoard.TaskId", task.Id);
|
|
DragAndDrop.StartDrag(task.Id);
|
|
current.Use();
|
|
}
|
|
|
|
if (current.type == EventType.MouseUp)
|
|
{
|
|
pressedTask = null;
|
|
}
|
|
}
|
|
|
|
private void HandleColumnDrop(Rect columnRect, string status)
|
|
{
|
|
Event current = Event.current;
|
|
if (!columnRect.Contains(current.mousePosition))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (current.type == EventType.DragUpdated)
|
|
{
|
|
if (GetDraggedTask() != null)
|
|
{
|
|
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
|
|
current.Use();
|
|
Repaint();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (current.type == EventType.DragPerform)
|
|
{
|
|
TaskRecord task = GetDraggedTask();
|
|
if (task == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DragAndDrop.AcceptDrag();
|
|
current.Use();
|
|
pressedTask = null;
|
|
|
|
if (!string.Equals(task.Status, status, StringComparison.Ordinal))
|
|
{
|
|
MoveTaskToStatus(task, status);
|
|
}
|
|
}
|
|
}
|
|
|
|
private TaskRecord GetDraggedTask()
|
|
{
|
|
string draggedTaskId = DragAndDrop.GetGenericData("TaskBoard.TaskId") as string;
|
|
if (string.IsNullOrEmpty(draggedTaskId) || data == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return data.Tasks.FirstOrDefault(task => string.Equals(task.Id, draggedTaskId, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private void MoveTaskToStatus(TaskRecord task, string status)
|
|
{
|
|
string selectedId = task.Id;
|
|
task.Status = status;
|
|
|
|
string error;
|
|
if (!TaskBoardService.Save(data, task, out error))
|
|
{
|
|
saveMessage = "Не удалось изменить статус перетаскиванием: " + error;
|
|
saveMessageType = MessageType.Error;
|
|
return;
|
|
}
|
|
|
|
saveMessage = task.FileExists
|
|
? "Статус задачи обновлен перетаскиванием."
|
|
: "Статус задачи обновлен в Index.md. Task-файл не найден, поэтому поля task-файла не обновлялись.";
|
|
saveMessageType = MessageType.Info;
|
|
Reload(selectedId);
|
|
}
|
|
|
|
private void SelectTask(TaskRecord task)
|
|
{
|
|
ResetDeleteConfirmation();
|
|
selectedTask = task;
|
|
TaskBoardService.LoadTaskDetails(selectedTask);
|
|
editStatus = selectedTask.Status;
|
|
editPriority = selectedTask.Priority;
|
|
editOwner = selectedTask.Owner;
|
|
editOwnerIsCustom = data != null && !data.OwnerPresets.Any(owner => string.Equals(owner, selectedTask.Owner, StringComparison.OrdinalIgnoreCase));
|
|
editExecutionTime = TaskBoardService.NormalizeExecutionTime(selectedTask.ExecutionTime);
|
|
editIndexSummary = selectedTask.IndexSummary;
|
|
editTaskSummary = selectedTask.TaskSummary;
|
|
editWhy = selectedTask.Why;
|
|
editExpectedOutcome = selectedTask.ExpectedOutcome;
|
|
editCurrentContext = selectedTask.CurrentContext;
|
|
editAcceptanceCriteria = selectedTask.AcceptanceCriteria;
|
|
editVerification = selectedTask.Verification;
|
|
editRisks = selectedTask.Risks;
|
|
editHumanDecisions = selectedTask.HumanDecisions;
|
|
editDecisionLog = selectedTask.DecisionLog;
|
|
editHandoffNotes = selectedTask.HandoffNotes;
|
|
}
|
|
|
|
private void SaveSelectedTask()
|
|
{
|
|
if (selectedTask == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string selectedId = selectedTask.Id;
|
|
selectedTask.Status = TaskBoardService.NormalizeStatus(editStatus);
|
|
selectedTask.Priority = editPriority;
|
|
selectedTask.Owner = string.IsNullOrWhiteSpace(editOwner) ? "unassigned" : editOwner.Trim();
|
|
selectedTask.ExecutionTime = TaskBoardService.NormalizeExecutionTime(editExecutionTime);
|
|
selectedTask.IndexSummary = (editIndexSummary ?? string.Empty).Trim();
|
|
if (selectedTask.FileExists)
|
|
{
|
|
selectedTask.TaskSummary = (editTaskSummary ?? string.Empty).Trim();
|
|
selectedTask.Why = (editWhy ?? string.Empty).Trim();
|
|
selectedTask.ExpectedOutcome = (editExpectedOutcome ?? string.Empty).Trim();
|
|
selectedTask.CurrentContext = (editCurrentContext ?? string.Empty).Trim();
|
|
selectedTask.AcceptanceCriteria = (editAcceptanceCriteria ?? string.Empty).Trim();
|
|
selectedTask.Verification = (editVerification ?? string.Empty).Trim();
|
|
selectedTask.Risks = (editRisks ?? string.Empty).Trim();
|
|
selectedTask.HumanDecisions = (editHumanDecisions ?? string.Empty).Trim();
|
|
selectedTask.DecisionLog = (editDecisionLog ?? string.Empty).Trim();
|
|
selectedTask.HandoffNotes = (editHandoffNotes ?? string.Empty).Trim();
|
|
}
|
|
|
|
string error;
|
|
if (!TaskBoardService.Save(data, selectedTask, out error))
|
|
{
|
|
saveMessage = "Не удалось сохранить задачу: " + error;
|
|
saveMessageType = MessageType.Error;
|
|
return;
|
|
}
|
|
|
|
saveMessage = selectedTask.FileExists
|
|
? "Изменения сохранены в Index.md и task-файл."
|
|
: "Изменения сохранены в Index.md. Task-файл не найден, поэтому поля task-файла не обновлялись.";
|
|
saveMessageType = MessageType.Info;
|
|
Reload(selectedId);
|
|
}
|
|
|
|
private void CreateSelectedTaskFromTemplate()
|
|
{
|
|
if (selectedTask == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string selectedId = selectedTask.Id;
|
|
string error;
|
|
if (!TaskBoardService.CreateTaskFileFromTemplate(data, selectedTask, out error))
|
|
{
|
|
saveMessage = "Не удалось создать task-файл из шаблона: " + error;
|
|
saveMessageType = MessageType.Error;
|
|
return;
|
|
}
|
|
|
|
saveMessage = "Task-файл создан по шаблону.";
|
|
saveMessageType = MessageType.Info;
|
|
Reload(selectedId);
|
|
}
|
|
|
|
private void DeleteSelectedTask()
|
|
{
|
|
if (selectedTask == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
string deletedId = selectedTask.Id;
|
|
string error;
|
|
if (!TaskBoardService.DeleteTask(data, selectedTask, out error))
|
|
{
|
|
saveMessage = "Не удалось удалить задачу: " + error;
|
|
saveMessageType = MessageType.Error;
|
|
return;
|
|
}
|
|
|
|
ResetDeleteConfirmation();
|
|
saveMessage = "Задача " + deletedId + " удалена.";
|
|
saveMessageType = MessageType.Info;
|
|
selectedTask = null;
|
|
Reload();
|
|
}
|
|
|
|
private bool HasChanges()
|
|
{
|
|
if (selectedTask == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool taskSummaryChanged = selectedTask.FileExists && !string.Equals(selectedTask.TaskSummary ?? string.Empty, editTaskSummary ?? string.Empty, StringComparison.Ordinal);
|
|
bool taskBodyChanged = selectedTask.FileExists
|
|
&& (!string.Equals(selectedTask.Why ?? string.Empty, editWhy ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.ExpectedOutcome ?? string.Empty, editExpectedOutcome ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.CurrentContext ?? string.Empty, editCurrentContext ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.AcceptanceCriteria ?? string.Empty, editAcceptanceCriteria ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.Verification ?? string.Empty, editVerification ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.Risks ?? string.Empty, editRisks ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.HumanDecisions ?? string.Empty, editHumanDecisions ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.DecisionLog ?? string.Empty, editDecisionLog ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.HandoffNotes ?? string.Empty, editHandoffNotes ?? string.Empty, StringComparison.Ordinal));
|
|
return !string.Equals(selectedTask.Status, editStatus, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.Priority, editPriority, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.Owner ?? string.Empty, editOwner ?? string.Empty, StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.ExecutionTime ?? string.Empty, TaskBoardService.NormalizeExecutionTime(editExecutionTime), StringComparison.Ordinal)
|
|
|| !string.Equals(selectedTask.IndexSummary ?? string.Empty, editIndexSummary ?? string.Empty, StringComparison.Ordinal)
|
|
|| taskSummaryChanged
|
|
|| taskBodyChanged;
|
|
}
|
|
|
|
private void Reload(string selectedId = null)
|
|
{
|
|
data = TaskBoardService.Load();
|
|
SyncSelections(selectedStatuses, TaskBoardConstants.Statuses);
|
|
SyncSelections(selectedPriorities, TaskBoardConstants.Priorities);
|
|
SyncSelections(selectedAreas, BuildAreaOptions());
|
|
SyncSelections(selectedOwners, BuildOwnerFilterOptions());
|
|
SyncSelections(selectedFileStates, new[] { "Existing", "Missing" });
|
|
SyncSelections(selectedWarningStates, new[] { "Warnings", "Clean" });
|
|
|
|
if (data == null || data.Tasks.Count == 0)
|
|
{
|
|
ResetDeleteConfirmation();
|
|
selectedTask = null;
|
|
return;
|
|
}
|
|
|
|
TaskRecord nextSelection = null;
|
|
if (!string.IsNullOrEmpty(selectedId))
|
|
{
|
|
nextSelection = data.Tasks.FirstOrDefault(task => string.Equals(task.Id, selectedId, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
if (nextSelection == null && selectedTask != null)
|
|
{
|
|
nextSelection = data.Tasks.FirstOrDefault(task => string.Equals(task.Id, selectedTask.Id, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
if (nextSelection == null)
|
|
{
|
|
selectedTask = null;
|
|
editStatus = string.Empty;
|
|
editPriority = string.Empty;
|
|
editOwner = string.Empty;
|
|
editExecutionTime = string.Empty;
|
|
editIndexSummary = string.Empty;
|
|
editTaskSummary = string.Empty;
|
|
editWhy = string.Empty;
|
|
editExpectedOutcome = string.Empty;
|
|
editCurrentContext = string.Empty;
|
|
editAcceptanceCriteria = string.Empty;
|
|
editVerification = string.Empty;
|
|
editRisks = string.Empty;
|
|
editHumanDecisions = string.Empty;
|
|
editDecisionLog = string.Empty;
|
|
editHandoffNotes = string.Empty;
|
|
return;
|
|
}
|
|
|
|
SelectTask(nextSelection);
|
|
}
|
|
|
|
private List<TaskRecord> GetFilteredTasks()
|
|
{
|
|
IEnumerable<TaskRecord> query = data != null ? data.Tasks : Enumerable.Empty<TaskRecord>();
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchText))
|
|
{
|
|
string search = searchText.Trim();
|
|
query = query.Where(task =>
|
|
ContainsIgnoreCase(task.Id, search)
|
|
|| ContainsIgnoreCase(task.Title, search)
|
|
|| ContainsIgnoreCase(task.IndexSummary, search)
|
|
|| ContainsIgnoreCase(task.Area, search)
|
|
|| ContainsIgnoreCase(task.Owner, search));
|
|
}
|
|
|
|
query = query.Where(task => selectedStatuses.Contains(task.Status));
|
|
query = query.Where(task => selectedPriorities.Contains(task.Priority));
|
|
query = query.Where(task => selectedAreas.Contains(string.IsNullOrWhiteSpace(task.Area) ? "-" : task.Area));
|
|
query = query.Where(task => selectedOwners.Contains(string.IsNullOrWhiteSpace(task.Owner) ? "unassigned" : task.Owner));
|
|
query = query.Where(task => selectedFileStates.Contains(task.FileExists ? "Existing" : "Missing"));
|
|
query = query.Where(task => selectedWarningStates.Contains(task.ValidationMessages.Count > 0 ? "Warnings" : "Clean"));
|
|
|
|
return ApplySort(query).ToList();
|
|
}
|
|
|
|
private string[] BuildAreaOptions()
|
|
{
|
|
if (data == null)
|
|
{
|
|
return Array.Empty<string>();
|
|
}
|
|
|
|
return data.Tasks
|
|
.Select(task => string.IsNullOrWhiteSpace(task.Area) ? "-" : task.Area)
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
.OrderBy(area => area, StringComparer.OrdinalIgnoreCase)
|
|
.ToArray();
|
|
}
|
|
|
|
private string[] BuildOwnerFilterOptions()
|
|
{
|
|
return TaskBoardService.BuildOwnerOptions(data, data != null ? data.Tasks : null);
|
|
}
|
|
|
|
private string[] BuildOwnerPresetOptions()
|
|
{
|
|
var options = new List<string>();
|
|
if (data != null)
|
|
{
|
|
options.AddRange(data.OwnerPresets.Where(owner => !string.IsNullOrWhiteSpace(owner)));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(editOwner) && !options.Any(owner => string.Equals(owner, editOwner, StringComparison.OrdinalIgnoreCase)) && !editOwnerIsCustom)
|
|
{
|
|
options.Insert(0, editOwner);
|
|
}
|
|
|
|
options.Add(CustomOwnerOption);
|
|
return options.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
|
}
|
|
|
|
private string[] BuildSortOptions()
|
|
{
|
|
return new[]
|
|
{
|
|
"Priority Desc",
|
|
"Priority Asc",
|
|
"Execution Time Desc",
|
|
"Execution Time Asc",
|
|
"ID Asc",
|
|
"ID Desc",
|
|
"Owner Asc",
|
|
"Title Asc",
|
|
"Area Asc",
|
|
};
|
|
}
|
|
|
|
private IEnumerable<TaskRecord> ApplySort(IEnumerable<TaskRecord> tasks)
|
|
{
|
|
switch (sortOption)
|
|
{
|
|
case "Priority Asc":
|
|
return tasks.OrderByDescending(task => TaskBoardService.GetPrioritySortOrder(task.Priority)).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "Execution Time Desc":
|
|
return tasks.OrderByDescending(task => task.EstimatedMinutes).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "Execution Time Asc":
|
|
return tasks.OrderBy(task => task.EstimatedMinutes < 0 ? int.MaxValue : task.EstimatedMinutes).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "ID Desc":
|
|
return tasks.OrderByDescending(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "Owner Asc":
|
|
return tasks.OrderBy(task => task.Owner, StringComparer.OrdinalIgnoreCase).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "Title Asc":
|
|
return tasks.OrderBy(task => task.Title, StringComparer.OrdinalIgnoreCase).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "Area Asc":
|
|
return tasks.OrderBy(task => task.Area, StringComparer.OrdinalIgnoreCase).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "ID Asc":
|
|
return tasks.OrderBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
case "Priority Desc":
|
|
default:
|
|
return tasks.OrderBy(task => TaskBoardService.GetPrioritySortOrder(task.Priority)).ThenBy(task => task.Id, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
private void DrawDetailsSplitter()
|
|
{
|
|
Rect splitterRect = GUILayoutUtility.GetRect(SplitterWidth, 1f, GUILayout.Width(SplitterWidth), GUILayout.ExpandHeight(true));
|
|
EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeHorizontal);
|
|
EditorGUI.DrawRect(splitterRect, new Color(0.2f, 0.2f, 0.2f, 0.75f));
|
|
|
|
Event current = Event.current;
|
|
switch (current.type)
|
|
{
|
|
case EventType.MouseDown:
|
|
if (splitterRect.Contains(current.mousePosition))
|
|
{
|
|
isResizingDetails = true;
|
|
current.Use();
|
|
}
|
|
break;
|
|
case EventType.MouseDrag:
|
|
if (isResizingDetails)
|
|
{
|
|
detailsWidth = Mathf.Clamp(position.width - current.mousePosition.x, MinDetailsWidth, Mathf.Max(MinDetailsWidth, position.width - MinBoardWidth));
|
|
Repaint();
|
|
current.Use();
|
|
}
|
|
break;
|
|
case EventType.MouseUp:
|
|
if (isResizingDetails)
|
|
{
|
|
isResizingDetails = false;
|
|
EditorPrefs.SetFloat(DetailsWidthPrefsKey, detailsWidth);
|
|
current.Use();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OpenOwnersConfig()
|
|
{
|
|
if (data == null || string.IsNullOrWhiteSpace(data.OwnersConfigPath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
UnityEngine.Object asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(data.OwnersConfigPath);
|
|
if (asset != null)
|
|
{
|
|
Selection.activeObject = asset;
|
|
EditorGUIUtility.PingObject(asset);
|
|
}
|
|
}
|
|
|
|
private int GetOwnerPopupIndex(string[] presetOptions)
|
|
{
|
|
if (editOwnerIsCustom)
|
|
{
|
|
return Array.IndexOf(presetOptions, CustomOwnerOption);
|
|
}
|
|
|
|
int index = Array.FindIndex(presetOptions, owner => string.Equals(owner, editOwner, StringComparison.OrdinalIgnoreCase));
|
|
return index >= 0 ? index : Array.IndexOf(presetOptions, CustomOwnerOption);
|
|
}
|
|
|
|
private string GetFormattedTime(TaskRecord task)
|
|
{
|
|
return task == null ? "-" : TaskBoardService.FormatExecutionTimeForDisplay(task.ExecutionTime);
|
|
}
|
|
|
|
private void SyncSelections(HashSet<string> selectedValues, string[] availableOptions)
|
|
{
|
|
var availableSet = new HashSet<string>(availableOptions ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
|
|
selectedValues.RemoveWhere(value => !availableSet.Contains(value));
|
|
if (selectedValues.Count == 0)
|
|
{
|
|
SetAll(selectedValues, availableOptions, true);
|
|
}
|
|
}
|
|
|
|
private void SetAll(HashSet<string> selectedValues, string[] values, bool enabled)
|
|
{
|
|
selectedValues.Clear();
|
|
if (!enabled || values == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (string value in values)
|
|
{
|
|
selectedValues.Add(value);
|
|
}
|
|
}
|
|
|
|
private static bool ContainsIgnoreCase(string source, string value)
|
|
{
|
|
return !string.IsNullOrEmpty(source) && source.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
private void ResetDeleteConfirmationIfExpired()
|
|
{
|
|
if (!string.IsNullOrEmpty(pendingDeleteTaskId) && EditorApplication.timeSinceStartup > pendingDeleteUntil)
|
|
{
|
|
ResetDeleteConfirmation();
|
|
}
|
|
}
|
|
|
|
private void ResetDeleteConfirmation()
|
|
{
|
|
pendingDeleteTaskId = null;
|
|
pendingDeleteUntil = 0d;
|
|
}
|
|
|
|
private static string SafeValue(string value)
|
|
{
|
|
return string.IsNullOrEmpty(value) ? "-" : value;
|
|
}
|
|
|
|
private static Color GetStatusColor(string status)
|
|
{
|
|
switch (TaskBoardService.NormalizeStatus(status))
|
|
{
|
|
case "BackLog":
|
|
return new Color(0.94f, 0.94f, 0.94f);
|
|
case "ToDo":
|
|
return new Color(0.85f, 0.96f, 0.85f);
|
|
case "InProgress":
|
|
return new Color(0.84f, 0.91f, 1f);
|
|
case "Review":
|
|
return new Color(1f, 0.9f, 0.82f);
|
|
case "Done":
|
|
return new Color(0.88f, 0.88f, 0.88f);
|
|
default:
|
|
return Color.white;
|
|
}
|
|
}
|
|
|
|
private static Color GetPriorityColor(string priority)
|
|
{
|
|
switch (TaskBoardService.NormalizePriority(priority))
|
|
{
|
|
case "Lowest":
|
|
return new Color(0.42f, 0.52f, 0.61f);
|
|
case "Low":
|
|
return new Color(0.45f, 0.45f, 0.45f);
|
|
case "Medium":
|
|
return new Color(0.19f, 0.48f, 0.84f);
|
|
case "High":
|
|
return new Color(0.88f, 0.54f, 0.16f);
|
|
case "Highest":
|
|
return new Color(0.78f, 0.2f, 0.2f);
|
|
default:
|
|
return new Color(0.35f, 0.35f, 0.35f);
|
|
}
|
|
}
|
|
}
|
|
}
|