JobDocumentSetTree.xaml.cs 73 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.ComponentModel;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Windows;
  9. using System.Windows.Controls;
  10. using System.Windows.Documents;
  11. using System.Windows.Forms;
  12. using System.Windows.Input;
  13. using System.Windows.Media;
  14. using com.sun.tools.javac.file;
  15. using Comal.Classes;
  16. using InABox.Clients;
  17. using InABox.Configuration;
  18. using InABox.Core;
  19. using InABox.DynamicGrid;
  20. using InABox.WPF;
  21. using net.sf.mpxj.phoenix.schema.phoenix5;
  22. using Syncfusion.Compression.Zip;
  23. using Syncfusion.Data.Extensions;
  24. using Syncfusion.UI.Xaml.Grid;
  25. using Syncfusion.UI.Xaml.TreeGrid;
  26. using Syncfusion.UI.Xaml.TreeGrid.Helpers;
  27. using JobDocumentSetFolder = Comal.Classes.JobDocumentSetFolder;
  28. using MessageBox = System.Windows.MessageBox;
  29. using UserControl = System.Windows.Controls.UserControl;
  30. namespace PRSDesktop
  31. {
  32. public class JobDocumentSetTreeSettings : UserConfigurationSettings
  33. {
  34. public bool DetailsVisible { get; set; }
  35. public JobDocumentSetTreeSettings()
  36. {
  37. DetailsVisible = true;
  38. }
  39. }
  40. public delegate void JobDocumentSetMileStoneSelected(JobDocumentSetMileStoneBlock block);
  41. public partial class JobDocumentSetTree : UserControl
  42. {
  43. public event JobDocumentSetMileStoneSelected MileStoneSelected;
  44. private struct MileStone
  45. {
  46. public Guid TypeID { get; set; }
  47. public CoreRow Row { get; set; }
  48. }
  49. private struct MileStoneType
  50. {
  51. public String Code { get; set; }
  52. public String Description { get; set; }
  53. public Dictionary<Guid,List<CoreRow>> SetMileStones { get; set; }
  54. public List<String> Columns { get; set; }
  55. }
  56. public Guid JobID { get; set; }
  57. public Guid[] FolderIDs{ get; set; }
  58. //public bool DisciplineVisible { get; set; }
  59. public Guid DisciplineID { get; set; }
  60. //public bool TypeVisible { get; set; }
  61. public Guid TypeID { get; set; }
  62. //public bool CategoryVisible { get; set; }
  63. public Guid CategoryID { get; set; }
  64. //public bool AreaVisible { get; set; }
  65. public Guid AreaID { get; set; }
  66. public String SearchText { get; set; }
  67. private Dictionary<Guid,MileStoneType> _types = null;
  68. private CoreTable _milestones = null;
  69. public CoreTable Data { get; private set; } = null;
  70. private CoreTable _files = null;
  71. private bool _hidesuperceded = false;
  72. private bool _flatlist = false;
  73. private bool _includeretired = false;
  74. private DocumentSetNodes _documentsets = null;
  75. private JobDocumentSetTreeSettings _settings;
  76. public JobDocumentSetTree()
  77. {
  78. InitializeComponent();
  79. AddImage.Source = PRSDesktop.Resources.add.AsBitmapImage();
  80. EditImage.Source = PRSDesktop.Resources.pencil.AsBitmapImage();
  81. DeleteImage.Source = PRSDesktop.Resources.delete.AsBitmapImage();
  82. _settings = new UserConfiguration<JobDocumentSetTreeSettings>().Load();
  83. treeGrid.Loaded += (o, e) =>
  84. {
  85. treeGrid.GetTreePanel().RowHeights[1] = 0;
  86. treeGrid.UpdateDataRow(1);
  87. };
  88. }
  89. public void Refresh()
  90. {
  91. using (new WaitCursor())
  92. {
  93. var scrollviewer = WPFUtils.FindVisualChildren<ScrollViewer>(treeGrid).FirstOrDefault();
  94. var verticalOffset = scrollviewer != null ? scrollviewer.VerticalOffset : 0;
  95. var horizontalOffset = treeGrid.SelectedItem != null ? scrollviewer.HorizontalOffset : 0;
  96. treeGrid.ItemsSource = null;
  97. var setfilter = new Filter<JobDocumentSet>(x => x.Job.ID).IsEqualTo(JobID);
  98. if ((FolderIDs?.Any() == true) && !FolderIDs.Contains(CoreUtils.FullGuid))
  99. setfilter = setfilter.And(x => x.Folder.ID).InList(FolderIDs);
  100. if (DisciplineID != Guid.Empty)
  101. setfilter = setfilter.And(x => x.Discipline.ID).IsEqualTo(DisciplineID);
  102. if (TypeID != Guid.Empty)
  103. setfilter = setfilter.And(x => x.Type.ID).IsEqualTo(TypeID);
  104. if (CategoryID != Guid.Empty)
  105. setfilter = setfilter.And(x => x.Category.ID).IsEqualTo(CategoryID);
  106. if (AreaID != Guid.Empty)
  107. setfilter = setfilter.And(x => x.Area.ID).IsEqualTo(AreaID);
  108. if (!_includeretired)
  109. setfilter = setfilter.And(x => x.Retired).IsEqualTo(DateTime.MinValue);
  110. if (!String.IsNullOrWhiteSpace(SearchText))
  111. setfilter = setfilter.TextSearch(SearchText, x => x.Code, x => x.Description);
  112. MultiQuery query = new MultiQuery();
  113. query.Add(
  114. setfilter,
  115. new Columns<JobDocumentSet>(x => x.ID)
  116. .Add(x => x.Parent.ID)
  117. .Add(x => x.Code)
  118. .Add(x => x.Description)
  119. .Add(x => x.Date)
  120. .Add(x => x.Size)
  121. .Add(x => x.Scale)
  122. .Add(x => x.Employee.Name)
  123. .Add(x=>x.Folder.ID)
  124. .Add(x=>x.Discipline.Description)
  125. .Add(x=>x.Category.Description)
  126. .Add(x=>x.Type.Description)
  127. .Add(x=>x.Area.Description),
  128. new SortOrder<JobDocumentSet>(x => x.Code)
  129. );
  130. var milestonefilter = new Filter<JobDocumentSetMileStone>(x => x.DocumentSet.Job.ID).IsEqualTo(JobID);
  131. if ((FolderIDs?.Any() == true) && !FolderIDs.Contains(CoreUtils.FullGuid))
  132. milestonefilter = milestonefilter.And(x => x.DocumentSet.Folder.ID).InList(FolderIDs);
  133. query.Add(
  134. milestonefilter,
  135. new Columns<JobDocumentSetMileStone>(x => x.ID)
  136. .Add(x => x.DocumentSet.ID)
  137. .Add(x => x.DocumentSet.Code)
  138. .Add(x => x.Type.ID)
  139. .Add(x => x.Type.Code)
  140. .Add(x => x.Status)
  141. .Add(x => x.Notes)
  142. .Add(x => x.Revision)
  143. .Add(x => x.Due)
  144. .Add(x => x.Submitted)
  145. .Add(x => x.Closed)
  146. .Add(x => x.Attachments)
  147. .Add(x => x.Watermark)
  148. );
  149. if (_types == null)
  150. {
  151. query.Add<JobDocumentSetMileStoneType>(
  152. null,
  153. new Columns<JobDocumentSetMileStoneType>(x => x.ID)
  154. .Add(x => x.Code)
  155. .Add(x => x.Description),
  156. new SortOrder<JobDocumentSetMileStoneType>(x => x.Sequence)
  157. );
  158. }
  159. query.Query();
  160. Data = query.Get<JobDocumentSet>();
  161. _milestones = query.Get<JobDocumentSetMileStone>();
  162. if (_types == null)
  163. {
  164. _types = query.Get<JobDocumentSetMileStoneType>().ToDictionary<JobDocumentSetMileStoneType, Guid, MileStoneType>(
  165. x => x.ID,
  166. r => new MileStoneType()
  167. {
  168. Code = r.Get<JobDocumentSetMileStoneType, String>(c => c.Code),
  169. Description = r.Get<JobDocumentSetMileStoneType, String>(c => c.Description),
  170. SetMileStones = new Dictionary<Guid, List<CoreRow>>(),
  171. Columns = new List<string>()
  172. }
  173. );
  174. }
  175. else
  176. {
  177. foreach (var typeid in _types.Keys)
  178. {
  179. _types[typeid].Columns.Clear();
  180. _types[typeid].SetMileStones.Clear();
  181. }
  182. }
  183. var milestones = _milestones.ToLookup<JobDocumentSetMileStone, Guid, MileStone>(
  184. x => x.DocumentSet.ID,
  185. r => new MileStone()
  186. {
  187. TypeID = r.Get<JobDocumentSetMileStone, Guid>(c => c.Type.ID),
  188. Row = r
  189. }
  190. );
  191. foreach (var milestone in milestones)
  192. {
  193. foreach (var entry in milestone)
  194. {
  195. if (!_types[entry.TypeID].SetMileStones.ContainsKey(milestone.Key))
  196. _types[entry.TypeID].SetMileStones[milestone.Key] = new List<CoreRow>();
  197. if (_hidesuperceded)
  198. _types[entry.TypeID].SetMileStones[milestone.Key].Clear();
  199. _types[entry.TypeID].SetMileStones[milestone.Key].Add(entry.Row);
  200. }
  201. }
  202. List<String> columns = new List<string>();
  203. foreach (var typeid in _types.Keys)
  204. {
  205. int count = 1;
  206. foreach (var setkey in _types[typeid].SetMileStones.Keys)
  207. count = Math.Max(count, _types[typeid].SetMileStones[setkey].Count);
  208. for (int i = 1; i <= count; i++)
  209. {
  210. String column = String.Format("{0}_{1}", _types[typeid].Code, i);
  211. columns.Add(column);
  212. _types[typeid].Columns.Add(String.Format("Blocks[{0}]", column));
  213. }
  214. }
  215. _documentsets = new DocumentSetNodes(columns);
  216. foreach (var setrow in Data.Rows)
  217. {
  218. Guid setid = setrow.Get<JobDocumentSet, Guid>(x => x.ID);
  219. Guid parentid = _flatlist ? Guid.Empty : setrow.Get<JobDocumentSet, Guid>(x => x.Parent.ID);
  220. String code = setrow.Get<JobDocumentSet, String>(c => c.Code);
  221. String description = setrow.Get<JobDocumentSet, String>(c => c.Description);
  222. var tags = new List<String>()
  223. {
  224. setrow.Get<JobDocumentSet, String>(c => c.Discipline.Description),
  225. setrow.Get<JobDocumentSet, String>(c => c.Type.Description),
  226. setrow.Get<JobDocumentSet, String>(c => c.Category.Description),
  227. setrow.Get<JobDocumentSet, String>(c => c.Area.Description)
  228. }.Where(x=>!String.IsNullOrWhiteSpace(x)).Distinct().ToArray();
  229. var node = _documentsets.Add(setid, parentid);
  230. JobDocumentSetDescriptionBlock desc = new JobDocumentSetDescriptionBlock(
  231. setid, code, description, tags);
  232. node.Description = Serialization.Serialize(desc);
  233. JobDocumentSetDetailsBlock dets = new JobDocumentSetDetailsBlock()
  234. {
  235. ID = setid,
  236. Date = setrow.Get<JobDocumentSet, DateTime>(c => c.Date),
  237. Size = setrow.Get<JobDocumentSet, PaperSize>(c => c.Size),
  238. Scale = setrow.Get<JobDocumentSet, String>(c => c.Scale),
  239. Employee = setrow.Get<JobDocumentSet, String>(c => c.Employee.Name)
  240. };
  241. node.Details = Serialization.Serialize(dets);
  242. foreach (var typeid in _types.Keys)
  243. {
  244. if (_types[typeid].SetMileStones.TryGetValue(setid, out var rows))
  245. {
  246. int i = 1;
  247. foreach (var row in rows)
  248. {
  249. JobDocumentSetMileStoneBlock block = new JobDocumentSetMileStoneBlock();
  250. block.ID = row.Get<JobDocumentSetMileStone, Guid>(c => c.ID);
  251. block.Revision = row.Get<JobDocumentSetMileStone, String>(c => c.Revision);
  252. block.Status = row.Get<JobDocumentSetMileStone, JobDocumentSetMileStoneStatus>(c => c.Status);
  253. block.Date = (block.Status == JobDocumentSetMileStoneStatus.Approved) ||
  254. (block.Status == JobDocumentSetMileStoneStatus.Cancelled) ||
  255. (block.Status == JobDocumentSetMileStoneStatus.Rejected)
  256. ? row.Get<JobDocumentSetMileStone, DateTime>(c => c.Closed)
  257. : block.Status == JobDocumentSetMileStoneStatus.Submitted
  258. ? block.Date = row.Get<JobDocumentSetMileStone, DateTime>(c => c.Submitted)
  259. : row.Get<JobDocumentSetMileStone, DateTime>(c => c.Due);
  260. String[] notes = row.Get<JobDocumentSetMileStone, String[]>(c => c.Notes);
  261. block.Notes = notes != null ? String.Join("\n", notes) : "";
  262. block.Attachments = row.Get<JobDocumentSetMileStone, int>(c => c.Attachments);
  263. block.Watermark = row.Get<JobDocumentSetMileStone, String>(c => c.Watermark);
  264. node.Blocks[String.Format("{0}_{1}", _types[typeid].Code, i)] = Serialization.Serialize(block);
  265. i++;
  266. }
  267. }
  268. }
  269. }
  270. ConfigureColumns(_documentsets);
  271. ConfigureStackedHeader();
  272. treeGrid.ItemsSource = _documentsets.Nodes;
  273. DocumentCount.Content = $"{_documentsets.Nodes.Count} {(_documentsets.Nodes.Count > 1 ? "Records" : "Record")}";
  274. if (scrollviewer != null)
  275. {
  276. scrollviewer.ScrollToVerticalOffset(verticalOffset);
  277. scrollviewer.ScrollToHorizontalOffset(horizontalOffset);
  278. }
  279. }
  280. }
  281. #region Grid Configuration
  282. private void ConfigureColumns(DocumentSetNodes documentsets)
  283. {
  284. treeGrid.Columns.Clear();
  285. treeGrid.Columns.Add(new TreeGridTemplateColumn()
  286. {
  287. CellTemplate = FindResource("descriptionTemplate") as DataTemplate,
  288. MappingName = "Description",
  289. SetCellBoundValue = true,
  290. MinimumWidth = 250,
  291. ColumnSizer = TreeColumnSizer.Star
  292. });
  293. treeGrid.Columns.Add(new TreeGridTemplateColumn()
  294. {
  295. CellTemplate = FindResource("detailsTemplate") as DataTemplate,
  296. MappingName = "Details",
  297. SetCellBoundValue = true,
  298. Width = _settings.DetailsVisible ? 120 : 0
  299. });
  300. foreach (var column in documentsets.Columns)
  301. {
  302. var col = new TreeGridTemplateColumn()
  303. {
  304. CellTemplate = FindResource("milestoneTemplate") as DataTemplate,
  305. MappingName = String.Format("Blocks[{0}]",column),
  306. SetCellBoundValue = true,
  307. HeaderText = " ",
  308. Width = 80,
  309. ShowToolTip = true
  310. };
  311. treeGrid.Columns.Add(col);
  312. }
  313. }
  314. private void ConfigureStackedHeader()
  315. {
  316. stackedHeaderRow.StackedColumns.Clear();
  317. stackedHeaderRow.StackedColumns.Add(new StackedColumn()
  318. {
  319. ChildColumns = "Description,Details",
  320. HeaderText = "Document Register"
  321. });
  322. foreach (var typeid in _types.Keys)
  323. {
  324. stackedHeaderRow.StackedColumns.Add(new StackedColumn()
  325. {
  326. ChildColumns = String.Join(",", _types[typeid].Columns),
  327. HeaderText = _types[typeid].Code
  328. });
  329. }
  330. }
  331. private void TreeGrid_OnItemsSourceChanged(object? sender, TreeGridItemsSourceChangedEventArgs e)
  332. {
  333. var panel = treeGrid.GetTreePanel();
  334. panel.RowHeights[1] = 0;
  335. }
  336. private void TreeGrid_OnNodeCollapsing(object? sender, NodeCollapsingEventArgs e)
  337. {
  338. e.Cancel = true;
  339. }
  340. public MenuItem CreateCalendar(ContextMenu menu, string text, DateTime startDate, CoreRow[] milestones, Action<CoreRow[], DateTime?>? action)
  341. {
  342. var item = new MenuItem();
  343. var calendarItem = new MenuItem();
  344. var calendar = new System.Windows.Controls.Calendar { DisplayDate = startDate, SelectedDate = null};
  345. calendar.SelectedDatesChanged += (o, e) =>
  346. {
  347. action?.Invoke(milestones, calendar.SelectedDate);
  348. menu.IsOpen = false;
  349. };
  350. calendarItem.Header = calendar;
  351. calendarItem.Style = DynamicGridUtils.Resources["NonHighlightMenuItem"] as Style;
  352. item.Header = text;
  353. item.Items.Add(calendarItem);
  354. item.IsCheckable = false;
  355. return item;
  356. }
  357. private void TreeGrid_OnContextMenuOpening(object sender, ContextMenuEventArgs e)
  358. {
  359. if (treeGrid.SelectedItem == null)
  360. {
  361. e.Handled = true;
  362. return;
  363. }
  364. MileStoneMenu.Items.Clear();
  365. var tag = (e.OriginalSource as FrameworkElement).Tag;
  366. Point pos = Mouse.GetPosition(treeGrid);
  367. var treeGridPanel = this.treeGrid.GetTreePanel();
  368. // get the row and column index based on the pointer position
  369. var rowColumnIndex = treeGridPanel.PointToCellRowColumnIndex(pos);
  370. if (rowColumnIndex.IsEmpty)
  371. return;
  372. var treeNodeAtRowIndex = treeGrid.GetNodeAtRowIndex(rowColumnIndex.RowIndex);
  373. if (rowColumnIndex.ColumnIndex < 2)
  374. {
  375. var documents = treeGrid.SelectedItems.Select(x => (x as DocumentSetNode)).ToArray();
  376. var ids = documents.Select(x => x.ID).ToArray();
  377. MenuItem edit = new MenuItem();
  378. edit.Header = "Edit Document Set";
  379. edit.Click += (o, args) => { EditDocumentSets(ids); };
  380. MileStoneMenu.Items.Add(edit);
  381. if (documents.Length == 1)
  382. {
  383. MileStoneMenu.Items.Add(new Separator());
  384. MenuItem addchild = new MenuItem();
  385. addchild.Header = "Add Child";
  386. addchild.Click += (o, args) => { AddChildDocument(documents.First()); };
  387. MileStoneMenu.Items.Add(addchild);
  388. }
  389. MenuItem movetofolder = new MenuItem();
  390. movetofolder.Header = "Move To Folder";
  391. bool hasfolders = PopulateFolders(movetofolder, documents);
  392. if (hasfolders)
  393. {
  394. MileStoneMenu.Items.Add(new Separator());
  395. MileStoneMenu.Items.Add(movetofolder);
  396. }
  397. MileStoneMenu.Items.Add(new Separator());
  398. MenuItem detailscolumn = new MenuItem();
  399. detailscolumn.Header = (treeGrid.Columns[1].Width > 0) ? "Hide Detail Column" : "Show Detail Column";
  400. detailscolumn.Click += ShowHideDetailsColumn;
  401. MileStoneMenu.Items.Add(detailscolumn);
  402. return;
  403. }
  404. var mappingname = treeGrid.Columns[rowColumnIndex.ColumnIndex].MappingName;
  405. var blockkey = mappingname.Replace("Blocks[", "").Replace("]", "");
  406. var typeid = _types.FirstOrDefault(x => x.Value.Columns.Contains(mappingname)).Key;
  407. //Guid setid = (treeGrid.SelectedItem as DocumentSetNode).ID;
  408. Guid[] setids = treeGrid.SelectedItems.Select(x => (x as DocumentSetNode).ID).ToArray();
  409. //Guid.TryParse(tag.ToString(), out Guid milestoneid);
  410. var blocks = treeGrid.SelectedItems.Select(x => (x as DocumentSetNode).Blocks[blockkey]).Where(x => !String.IsNullOrWhiteSpace(x))
  411. .ToArray();
  412. var milestoneids = blocks.Select(x => Serialization.Deserialize<JobDocumentSetMileStoneBlock>(x).ID).ToArray();
  413. //var milestone = _milestones.Rows.FirstOrDefault(r => r.Get<JobDocumentSetMileStone, Guid>(c => c.ID) == milestoneid);
  414. var milestones = _milestones.Rows.Where(r => milestoneids.Contains(r.Get<JobDocumentSetMileStone, Guid>(c => c.ID))).ToArray();
  415. bool canCreateNewMileStones = true;
  416. foreach (var setid in setids)
  417. {
  418. var openmilestones = _milestones.Rows.Any(r =>
  419. Guid.Equals(r.Get<JobDocumentSetMileStone, Guid>(c => c.DocumentSet.ID), setid)
  420. && Guid.Equals(r.Get<JobDocumentSetMileStone, Guid>(c => c.Type.ID), typeid)
  421. && (r.Get<JobDocumentSetMileStone, DateTime>(c => c.Closed).IsEmpty() ||
  422. (r.Get<JobDocumentSetMileStone, JobDocumentSetMileStoneStatus>(c => c.Status) == JobDocumentSetMileStoneStatus.Approved))
  423. );
  424. if (openmilestones)
  425. canCreateNewMileStones = false;
  426. }
  427. if (canCreateNewMileStones)
  428. {
  429. MenuItem newmilestone = new MenuItem()
  430. {
  431. Header = "New Milestone",
  432. Tag = typeid
  433. };
  434. newmilestone.Click += (o, args) => { CreateMileStone(setids, typeid, DateTime.Today); };
  435. MileStoneMenu.Items.Add(newmilestone);
  436. }
  437. if (milestones.Any())
  438. {
  439. MenuItem setstatus = new MenuItem() { Header = "Change Status" };
  440. foreach (JobDocumentSetMileStoneStatus newstatus in Enum.GetValues(typeof(JobDocumentSetMileStoneStatus)))
  441. {
  442. MenuItem setstatus2 = null;
  443. switch (newstatus)
  444. {
  445. case JobDocumentSetMileStoneStatus.Unknown:
  446. break;
  447. case JobDocumentSetMileStoneStatus.NotStarted:
  448. case JobDocumentSetMileStoneStatus.InProgress:
  449. case JobDocumentSetMileStoneStatus.OnHold:
  450. case JobDocumentSetMileStoneStatus.InfoRequired:
  451. setstatus2 = new MenuItem() { Header = newstatus.ToString().SplitCamelCase() };
  452. setstatus2.Click += (o, args) => { ChangeMileStoneStatus(milestones, newstatus, DateTime.MinValue, DateTime.MinValue); };
  453. break;
  454. case JobDocumentSetMileStoneStatus.Submitted:
  455. setstatus2 = CreateCalendar(
  456. MileStoneMenu,
  457. newstatus.ToString().SplitCamelCase(),
  458. DateTime.Today,
  459. milestones,
  460. (r, t) => { ChangeMileStoneStatus(milestones, newstatus, t, DateTime.MinValue); }
  461. );
  462. break;
  463. case JobDocumentSetMileStoneStatus.Approved:
  464. case JobDocumentSetMileStoneStatus.Cancelled:
  465. case JobDocumentSetMileStoneStatus.Rejected:
  466. setstatus2 = CreateCalendar(
  467. MileStoneMenu,
  468. newstatus.ToString().SplitCamelCase(),
  469. DateTime.Today,
  470. milestones,
  471. (r, t) => { ChangeMileStoneStatus(milestones, newstatus, null, t); }
  472. );
  473. break;
  474. }
  475. if (setstatus2 != null)
  476. setstatus.Items.Add(setstatus2);
  477. }
  478. MileStoneMenu.Items.Add(setstatus);
  479. MenuItem editmilestone = new MenuItem() { Header = "Edit MileStone" };
  480. editmilestone.Click += (o, args) => { EditMileStones(milestones); };
  481. MileStoneMenu.Items.Add(editmilestone);
  482. //var closed = milestones.Any(r => !r.Get<JobDocumentSetMileStone, DateTime>(c => c.Closed).IsEmpty());
  483. if ((setids.Length == 1) && (milestones.Length == 1)) // && !closed)
  484. {
  485. var attachments = milestones[0].Get<JobDocumentSetMileStone, int>(x => x.Attachments);
  486. if (attachments > 1)
  487. {
  488. MenuItem splitmilestone = new MenuItem() { Header = "Split MileStone" };
  489. splitmilestone.Click += (o, args) => { SplitMileStone(setids[0], milestones[0]); };
  490. MileStoneMenu.Items.Add(splitmilestone);
  491. }
  492. }
  493. if (milestones.Any())
  494. {
  495. MileStoneMenu.Items.Add(new Separator());
  496. MenuItem upload = new MenuItem() { Header = milestones.Length > 1 ? "Upload and Match File Names" : "Upload Files" };
  497. upload.Click += (o, args) => { UploadFiles(milestones); };
  498. MileStoneMenu.Items.Add(upload);
  499. MenuItem download = new MenuItem() { Header = "Download Files" };
  500. download.Items.Add(new MenuItem());
  501. download.SubmenuOpened += (o, e) =>
  502. {
  503. download.Items.Clear();
  504. var files = new Client<JobDocumentSetMileStoneFile>().Query(
  505. new Filter<JobDocumentSetMileStoneFile>(x => x.EntityLink.ID).InList(milestoneids),
  506. new Columns<JobDocumentSetMileStoneFile>(x => x.ID)
  507. .Add(x => x.DocumentLink.FileName)
  508. .Add(x => x.DocumentLink.ID),
  509. new SortOrder<JobDocumentSetMileStoneFile>(x => x.DocumentLink.FileName)
  510. );
  511. if (files.Rows.Any())
  512. {
  513. foreach (var row in files.Rows)
  514. {
  515. MenuItem downloadone = new MenuItem()
  516. {
  517. Header = row.Get<JobDocumentSetMileStoneFile, String>(x => x.DocumentLink.FileName),
  518. };
  519. downloadone.Click += (sender, args) =>
  520. {
  521. DownloadFiles(
  522. new CoreRow[] { milestones[0] },
  523. row.Get<JobDocumentSetMileStoneFile, Guid>(x => x.DocumentLink.ID)
  524. );
  525. };
  526. download.Items.Add(downloadone);
  527. }
  528. if (download.Items.Count > 1)
  529. {
  530. download.Items.Add(new Separator());
  531. MenuItem downloadall = new MenuItem()
  532. {
  533. Header = "Download All",
  534. };
  535. downloadall.Click += (sender, args) =>
  536. {
  537. DownloadFiles(
  538. milestones,
  539. Guid.Empty
  540. );
  541. };
  542. download.Items.Add(downloadall);
  543. }
  544. }
  545. else
  546. {
  547. download.Items.Add(
  548. new MenuItem()
  549. {
  550. Header = "No Files to download",
  551. IsEnabled = false
  552. }
  553. );
  554. }
  555. };
  556. MileStoneMenu.Items.Add(download);
  557. }
  558. // if ((milestoneids.Length == 1)) // && !closed)
  559. // {
  560. // MenuItem managefiles = new MenuItem()
  561. // {
  562. // Header = "Manage Files"
  563. // };
  564. // managefiles.Click += (sender, args) => { ManageFiles(milestones[0]); };
  565. // MileStoneMenu.Items.Add(managefiles);
  566. // }
  567. MileStoneMenu.Items.Add(new Separator());
  568. MenuItem export = new MenuItem { Header = "Export Files" };
  569. export.Click += (o, args) => ExportFiles(milestones);
  570. MileStoneMenu.Items.Add(export);
  571. MileStoneMenu.Items.Add(new Separator());
  572. MenuItem delete = new MenuItem { Header = "Delete MileStone" };
  573. delete.Click += (o, args) => DeleteMileStone(milestones);
  574. MileStoneMenu.Items.Add(delete);
  575. }
  576. if (MileStoneMenu.Items.Count == 0)
  577. e.Handled = true;
  578. }
  579. private void ExportFiles(CoreRow[] milestones)
  580. {
  581. SaveFileDialog sfd = new SaveFileDialog();
  582. sfd.Filter = "Compressed Files (*.zip)|*.zip";
  583. sfd.AddExtension = true;
  584. if (sfd.ShowDialog() != DialogResult.OK)
  585. return;
  586. Progress.ShowModal("Exporting Files", (progress) =>
  587. {
  588. progress.Report("Getting File Links");
  589. var milestoneids = milestones.Select(r => r.Get<JobDocumentSetMileStone, Guid>(c => c.ID)).ToArray();
  590. var links = new Client<JobDocumentSetMileStoneFile>().Query(
  591. new Filter<JobDocumentSetMileStoneFile>(c => c.EntityLink.ID).InList(milestoneids),
  592. new Columns<JobDocumentSetMileStoneFile>(x => x.EntityLink.ID)
  593. .Add(x=>x.EntityLink.DocumentSet.Code)
  594. .Add(x=>x.EntityLink.Type.Code)
  595. .Add(x=>x.EntityLink.Revision)
  596. .Add(x=>x.EntityLink.Status)
  597. .Add(x => x.DocumentLink.ID)
  598. .Add(x=>x.DocumentLink.FileName)
  599. );
  600. Syncfusion.Compression.Zip.ZipArchive zip = new Syncfusion.Compression.Zip.ZipArchive();
  601. int i = 0;
  602. foreach (var row in links.Rows)
  603. {
  604. i++;
  605. String code = row.Get<JobDocumentSetMileStoneFile, String>(c => c.EntityLink.DocumentSet.Code);
  606. String filename = Path.GetFileNameWithoutExtension(row.Get<JobDocumentSetMileStoneFile, String>(c => c.DocumentLink.FileName));
  607. String extension = Path.GetExtension(row.Get<JobDocumentSetMileStoneFile, String>(c => c.DocumentLink.FileName));
  608. String type = $"{row.Get<JobDocumentSetMileStoneFile,String>(c=>c.EntityLink.Type.Code)} {row.Get<JobDocumentSetMileStoneFile,String>(c=>c.EntityLink.Revision)}".Trim();
  609. var status = row.Get<JobDocumentSetMileStoneFile, JobDocumentSetMileStoneStatus>(c => c.EntityLink.Status);
  610. filename = $"{code}/{filename} {type} ({status}){extension}";
  611. progress.Report($"Processing {i} of {links.Rows.Count} files");
  612. Guid docid = row.Get<JobDocumentSetMileStoneFile, Guid>(c => c.DocumentLink.ID);
  613. var data = new Client<Document>().Query(
  614. new Filter<Document>(x => x.ID).IsEqualTo(docid),
  615. new Columns<Document>(x=>x.ID).Add(x => x.Data)
  616. ).Rows.Select(r=>r.Get<Document,byte[]>(c=>c.Data)).FirstOrDefault();
  617. if (data != null)
  618. {
  619. var item = new ZipArchiveItem(zip, filename, new MemoryStream(data), true, FileAttributes.Normal);
  620. zip.AddItem(item);
  621. }
  622. }
  623. progress.Report("Closing archive");
  624. zip.Save(sfd.FileName);
  625. zip.Close();
  626. });
  627. MessageBox.Show("All Done!");
  628. }
  629. private void ShowHideDetailsColumn(object sender, RoutedEventArgs e)
  630. {
  631. _settings.DetailsVisible = !_settings.DetailsVisible;
  632. new UserConfiguration<JobDocumentSetTreeSettings>().Save(_settings);
  633. treeGrid.Columns[1].Width = _settings.DetailsVisible ? 120: 0;
  634. }
  635. private bool PopulateFolders(MenuItem menu, IEnumerable<DocumentSetNode> documents)
  636. {
  637. CoreTable data = new Client<JobDocumentSetFolder>().Query(
  638. new Filter<JobDocumentSetFolder>(x => x.Job.ID).IsEqualTo(JobID),
  639. new Columns<JobDocumentSetFolder>(x => x.ID)
  640. .Add(x => x.Parent.ID)
  641. .Add(x => x.Name)
  642. );
  643. if (!data.Rows.Any())
  644. return false;
  645. DynamicTreeNodes folders = new DynamicTreeNodes();
  646. folders.Load<JobDocumentSetFolder>(data, x => x.ID, x => x.Parent.ID, x => x.Name);
  647. foreach (var folder in folders.Nodes)
  648. DoPopulateFolder(menu, folder, documents);
  649. return true;
  650. }
  651. private void DoPopulateFolder(MenuItem header, DynamicTreeNode folder, IEnumerable<DocumentSetNode> documents)
  652. {
  653. MenuItem menu = new MenuItem();
  654. menu.Header = folder.Description;
  655. menu.Click += (sender, args) => MoveToFolder(documents, folder);
  656. header.Items.Add(menu);
  657. foreach (var childfolder in folder.Children)
  658. DoPopulateFolder(menu, childfolder, documents);
  659. }
  660. private void MoveToFolder(IEnumerable<DocumentSetNode> documents, DynamicTreeNode folder)
  661. {
  662. using (new WaitCursor())
  663. {
  664. List<JobDocumentSet> updates = new List<JobDocumentSet>();
  665. foreach (var document in documents)
  666. {
  667. var folderid = Data.Rows.FirstOrDefault(r => r.Get<JobDocumentSet, Guid>(c => c.ID) == document.ID)?.Get<JobDocumentSet, Guid>(c => c.Folder.ID) ?? Guid.Empty;
  668. if (folderid != folder.ID)
  669. {
  670. var update = new JobDocumentSet();
  671. update.ID = document.ID;
  672. update.CommitChanges();
  673. update.Folder.ID = folder.ID;
  674. update.Parent.ID = Guid.Empty;
  675. updates.Add(update);
  676. }
  677. }
  678. if (updates.Any())
  679. new Client<JobDocumentSet>().Save(updates, "Moved to Folder: " + folder.Description);
  680. else
  681. MessageBox.Show("Nothing to Do!");
  682. }
  683. Refresh();
  684. }
  685. private void SplitMileStone(Guid setid, CoreRow milestone)
  686. {
  687. if (MessageBox.Show(
  688. "Are you sure you wish to split this Document Set?",
  689. "Confirm Delete",
  690. MessageBoxButton.YesNo
  691. ) != MessageBoxResult.Yes)
  692. return;
  693. Guid milestoneid = milestone.Get<JobDocumentSetMileStone, Guid>(c => c.ID);
  694. var dlg = new MultiSelectDialog<JobDocumentSetMileStoneFile>(
  695. new Filter<JobDocumentSetMileStoneFile>(c => c.EntityLink.ID).IsEqualTo(milestoneid),
  696. null,
  697. true
  698. );
  699. if (dlg.ShowDialog() == true)
  700. {
  701. var files = dlg.Items();
  702. Progress.ShowModal("Splitting Document Set", (progress) =>
  703. {
  704. JobDocumentSet newset = new Client<JobDocumentSet>().Query(
  705. new Filter<JobDocumentSet>(x => x.ID).IsEqualTo(setid)
  706. ).Rows.FirstOrDefault()?.ToObject<JobDocumentSet>();
  707. if (newset != null)
  708. {
  709. newset.ID = Guid.Empty;
  710. newset.CommitChanges();
  711. newset.Parent.ID = setid;
  712. newset.Code = String.Format("{0} (COPY)", newset.Code);
  713. //newset.Description = "New Child";
  714. new Client<JobDocumentSet>().Save(newset, "Created by Splitting MileStone");
  715. progress.Report("Creating Milestone");
  716. JobDocumentSetMileStone newms = new Client<JobDocumentSetMileStone>().Query(
  717. new Filter<JobDocumentSetMileStone>(c=>c.ID).IsEqualTo(milestoneid)
  718. ).Rows.FirstOrDefault()?.ToObject<JobDocumentSetMileStone>();
  719. if (newms != null)
  720. {
  721. newms.ID = Guid.Empty;
  722. newset.CommitChanges();
  723. newms.DocumentSet.ID = newset.ID;
  724. new Client<JobDocumentSetMileStone>().Save(newms, "Created By Splitting MileStone");
  725. progress.Report("Moving Files");
  726. foreach (var file in files)
  727. file.EntityLink.ID = newms.ID;
  728. new Client<JobDocumentSetMileStoneFile>().Save(files, "Moved when Splitting MileStone");
  729. }
  730. }
  731. });
  732. Refresh();
  733. }
  734. }
  735. private void AddChildDocument(DocumentSetNode node)
  736. {
  737. if (node == null)
  738. return;
  739. var folderid = Data.Rows.FirstOrDefault(r => r.Get<JobDocumentSet, Guid>(c => c.ID) == node.ID)?.Get<JobDocumentSet, Guid>(c => c.Folder.ID) ?? Guid.Empty;
  740. JobDocumentSet newset = new JobDocumentSet();
  741. newset.Parent.ID = node.ID;
  742. newset.Job.ID = JobID;
  743. newset.Folder.ID = folderid;
  744. var grid = new DynamicDataGrid<JobDocumentSet>();
  745. if (grid.EditItems(new[] { newset }))
  746. Refresh();
  747. }
  748. // private void ManageFiles(CoreRow milestone)
  749. // {
  750. // var grid = new JobDocumentSetMileStoneFileGrid();
  751. // grid.OnGetWaterMark += (row) => milestone.Get<JobDocumentSetMileStone, String>(c => c.Watermark);
  752. // grid.ShowSupercededColumn = false;
  753. // Window window = new Window();
  754. // window.Padding = new Thickness(5);
  755. // window.Content = grid;
  756. // window.Width = 300;
  757. // window.Height = 500;
  758. // window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
  759. // grid.Load(milestone.ToObject<JobDocumentSetMileStone>(), null);
  760. // grid.Margin = new Thickness(5);
  761. // grid.Refresh(true, true);
  762. // window.ShowDialog();
  763. // Refresh();
  764. // }
  765. private void DownloadFiles(CoreRow[] rows, Guid id)
  766. {
  767. FolderBrowserDialog dlg = new FolderBrowserDialog();
  768. if (dlg.ShowDialog() == DialogResult.OK)
  769. {
  770. Progress.ShowModal("Downloading Files", (progress) =>
  771. {
  772. foreach (var row in rows)
  773. {
  774. var status = row.Get<JobDocumentSetMileStone, JobDocumentSetMileStoneStatus>(c => c.Status);
  775. var stage = row.Get<JobDocumentSetMileStone, String>(c => c.Type.Code);
  776. var revision = row.Get<JobDocumentSetMileStone, String>(c => c.Revision);
  777. String tag = String.Format(" - {0}{1} ({2})", stage, String.IsNullOrWhiteSpace(revision) ? "" : " - Rev " + revision,
  778. status.ToString().SplitCamelCase());
  779. var filter = id == Guid.Empty
  780. ? new Filter<Document>(x => x.ID).InQuery(
  781. new Filter<JobDocumentSetMileStoneFile>(x => x.EntityLink.ID).IsEqualTo(
  782. row.Get<JobDocumentSetMileStone, Guid>(c => c.ID)),
  783. x => x.DocumentLink.ID
  784. )
  785. : new Filter<Document>(x => x.ID).IsEqualTo(id);
  786. var files = new Client<Document>().Query(filter);
  787. foreach (var filerow in files.Rows)
  788. {
  789. string filename = filerow.Get<Document, String>(c => c.FileName);
  790. string extension = Path.GetExtension(filename);
  791. string basefilename = Path.GetFileNameWithoutExtension(filename);
  792. filename = String.Format("{0}{1}{2}", basefilename, tag, extension);
  793. filename = Path.Combine(dlg.SelectedPath, filename);
  794. File.WriteAllBytes(filename, filerow.Get<Document, byte[]>(c => c.Data));
  795. }
  796. }
  797. });
  798. Process.Start(new ProcessStartInfo(dlg.SelectedPath) { UseShellExecute = true });
  799. }
  800. }
  801. private bool SelectFiles(out String[] files)
  802. {
  803. Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog();
  804. dlg.Filter = "PDF Files (*.pdf)|*.pdf";
  805. dlg.Multiselect = true;
  806. if ((dlg.ShowDialog() == true) && (dlg.FileNames.Length > 0))
  807. {
  808. files = dlg.FileNames.ToArray();
  809. return true;
  810. }
  811. files = null;
  812. return false;
  813. }
  814. private void UploadFiles(CoreRow[] rows)
  815. {
  816. if (rows?.Length < 1)
  817. {
  818. MessageBox.Show("No Rows Selected");
  819. return;
  820. }
  821. if (SelectFiles(out String[] filenames))
  822. {
  823. Dictionary<String, Guid>? setlookups = rows.Length > 1
  824. ? new Dictionary<string, Guid>(
  825. rows.Select(
  826. r => new KeyValuePair<String, Guid>(
  827. r.Get<JobDocumentSetMileStone, String>(c => c.DocumentSet.Code),
  828. r.Get<JobDocumentSetMileStone, Guid>(c => c.ID)
  829. )
  830. )
  831. )
  832. : null;
  833. if ((setlookups != null) && (rows.Length > 1))
  834. {
  835. var unmatched = filenames.Where(filename => !setlookups.Keys.Any(key => Path.GetFileName(filename).ToLower().ToLower().StartsWith(key.ToLower())));
  836. if (unmatched.Any())
  837. {
  838. MessageBox.Show("Unable to match the following files:\n" + String.Join("\n", unmatched));
  839. return;
  840. }
  841. }
  842. var milestoneids = rows.Select(r => r.Get<JobDocumentSetMileStone, Guid>(c => c.ID)).ToArray();
  843. var currentfiles = new Client<JobDocumentSetMileStoneFile>().Query(
  844. new Filter<JobDocumentSetMileStoneFile>(x => x.EntityLink.ID).InList(milestoneids),
  845. new Columns<JobDocumentSetMileStoneFile>(x => x.DocumentLink.ID)
  846. .Add(x => x.DocumentLink.FileName)
  847. ).ToDictionary<JobDocumentSetMileStoneFile, String, Guid>(x => x.DocumentLink.FileName, x => x.DocumentLink.ID);
  848. var matched = filenames.Where(filename => currentfiles.Keys.Any(key => Path.GetFileName(filename).ToLower().StartsWith(key.ToLower())));
  849. bool replace = false;
  850. if (matched.Any())
  851. {
  852. var confirm = MessageBox.Show(
  853. "The following files already exist!\n\n Do you wish to replace them?\n\n" + String.Join("\n", matched),
  854. "Replace Files",
  855. MessageBoxButton.YesNoCancel);
  856. if (confirm == MessageBoxResult.Cancel)
  857. return;
  858. replace = confirm == MessageBoxResult.Yes;
  859. }
  860. int doccount = 0;
  861. Progress.ShowModal("Uploading Files", (progress) =>
  862. {
  863. List<Document> documents = new List<Document>();
  864. foreach (var file in filenames)
  865. {
  866. if (!matched.Contains(file) || replace)
  867. {
  868. var filename = Path.GetFileName(file).ToLower();
  869. var code = currentfiles.Keys.FirstOrDefault(key => filename.StartsWith(key.ToLower()));
  870. var doc = new Document();
  871. doc.ID = String.IsNullOrWhiteSpace(code) ? Guid.Empty : currentfiles[code];
  872. doc.CommitChanges();
  873. doc.Data = File.ReadAllBytes(file);
  874. doc.FileName = filename;
  875. doc.CRC = CoreUtils.CalculateCRC(doc.Data);
  876. doc.TimeStamp = new FileInfo(file).LastWriteTime;
  877. documents.Add(doc);
  878. }
  879. }
  880. if (documents.Any())
  881. new Client<Document>().Save(documents.ToArray(), "Uploaded by User");
  882. progress.Report("Updating Links");
  883. List<JobDocumentSetMileStoneFile> links = new List<JobDocumentSetMileStoneFile>();
  884. foreach (var document in documents)
  885. {
  886. if (!currentfiles.Any(x => x.Value == document.ID))
  887. {
  888. var link = new JobDocumentSetMileStoneFile();
  889. if (setlookups != null)
  890. {
  891. var filename = Path.GetFileName(document.FileName).ToLower();
  892. var code = setlookups.Keys.FirstOrDefault(key => filename.StartsWith(key.ToLower()));
  893. link.EntityLink.ID = setlookups[code];
  894. }
  895. else
  896. link.EntityLink.ID = rows.First().Get<JobDocumentSetMileStone, Guid>(c => c.ID);
  897. link.DocumentLink.ID = document.ID;
  898. links.Add(link);
  899. }
  900. }
  901. if (links.Any())
  902. new Client<JobDocumentSetMileStoneFile>().Save(links, "Uploaded By User");
  903. doccount = documents.Count;
  904. });
  905. MessageBox.Show(String.Format("{0} Files Uploaded", doccount > 0 ? doccount : "No"));
  906. Refresh();
  907. }
  908. }
  909. private Dictionary<Guid, JobDocumentSetMileStone> GetPreviousMileStones(Guid[] setids, Guid typeid)
  910. {
  911. var result = new Dictionary<Guid, JobDocumentSetMileStone>();
  912. foreach (var setid in setids)
  913. {
  914. var typeindex = _types.Keys.IndexOf(typeid);
  915. JobDocumentSetMileStone? last = null;
  916. while ((last == null) && (typeindex > 0))
  917. {
  918. last = _milestones.Rows.LastOrDefault(r =>
  919. (r.Get<JobDocumentSetMileStone, Guid>(c => c.DocumentSet.ID) == setid) &&
  920. (r.Get<JobDocumentSetMileStone, Guid>(c => c.Type.ID) == _types.Keys.ToArray()[typeindex]))
  921. ?.ToObject<JobDocumentSetMileStone>();
  922. typeindex--;
  923. }
  924. if (last != null)
  925. result[setid] = last;
  926. }
  927. return result;
  928. }
  929. private void CreateMileStone(Guid[] setids, Guid typeid, DateTime duedate)
  930. {
  931. bool bCopy = false;
  932. var lastmilestones = GetPreviousMileStones(setids, typeid);
  933. if (lastmilestones.Any(x => x.Value.Attachments > 0))
  934. {
  935. var confirm = MessageBox.Show("Do you wish to copy the files from the previous milestones?", "Copy Files",
  936. MessageBoxButton.YesNoCancel);
  937. if (confirm == MessageBoxResult.Cancel)
  938. return;
  939. bCopy = confirm == MessageBoxResult.Yes;
  940. }
  941. Dictionary<JobDocumentSetMileStone,JobDocumentSetMileStoneFile[]> updates = new Dictionary<JobDocumentSetMileStone, JobDocumentSetMileStoneFile[]>();
  942. foreach (var setid in setids)
  943. {
  944. JobDocumentSetMileStone milestone = new JobDocumentSetMileStone();
  945. milestone.DocumentSet.ID = setid;
  946. milestone.Type.ID = typeid;
  947. milestone.Status = JobDocumentSetMileStoneStatus.NotStarted;
  948. milestone.Due = duedate;
  949. JobDocumentSetMileStoneFile[] files = new JobDocumentSetMileStoneFile[] { };
  950. if (bCopy && lastmilestones.TryGetValue(setid, out var lastmilestone))
  951. {
  952. if (lastmilestone.Attachments > 0)
  953. {
  954. files = new Client<JobDocumentSetMileStoneFile>().Query(
  955. new Filter<JobDocumentSetMileStoneFile>(x => x.EntityLink.ID).InList(lastmilestone.ID),
  956. new Columns<JobDocumentSetMileStoneFile>(x=>x.EntityLink.DocumentSet.ID)
  957. .Add(x => x.DocumentLink.FileName)
  958. .Add(x => x.DocumentLink.ID),
  959. new SortOrder<JobDocumentSetMileStoneFile>(x => x.DocumentLink.FileName)
  960. ).Rows.Select(r=>r.ToObject<JobDocumentSetMileStoneFile>()).ToArray();
  961. }
  962. }
  963. updates[milestone] = files;
  964. }
  965. var grid = new JobDocumentSetMileStoneGrid();
  966. grid.OnAfterSave += (editor, items) =>
  967. {
  968. if (updates.Keys.Count == 1)
  969. return;
  970. List<JobDocumentSetMileStoneFile> fileupdates = new List<JobDocumentSetMileStoneFile>();
  971. foreach (var milestone in updates.Keys)
  972. {
  973. foreach (var file in updates[milestone])
  974. {
  975. file.EntityLink.ID = milestone.ID;
  976. fileupdates.Add(file);
  977. }
  978. }
  979. if (fileupdates.Any())
  980. new Client<JobDocumentSetMileStoneFile>().Save(fileupdates,"");
  981. };
  982. if (grid.EditItems(updates.Keys.ToArray(), (t) =>
  983. {
  984. if ((t == typeof(JobDocumentSetMileStoneFile)) && (updates.Keys.Count == 1))
  985. {
  986. CoreTable result = new CoreTable();
  987. result.LoadColumns(typeof(JobDocumentSetMileStoneFile));
  988. result.LoadRows(updates[updates.Keys.First()]);
  989. return result;
  990. }
  991. return null;
  992. }, true)
  993. )
  994. Refresh();
  995. }
  996. private void ChangeMileStoneStatus(CoreRow[] rows, JobDocumentSetMileStoneStatus newstatus, DateTime? issued, DateTime? closed)
  997. {
  998. var milestones = rows.Select(r=>r.ToObject<JobDocumentSetMileStone>()).ToArray();
  999. foreach (var milestone in milestones)
  1000. {
  1001. if (issued.HasValue)
  1002. milestone.Submitted = issued.Value;
  1003. if (closed.HasValue)
  1004. milestone.Closed = closed.Value;
  1005. milestone.Status = newstatus;
  1006. }
  1007. using (new WaitCursor())
  1008. new Client<JobDocumentSetMileStone>().Save(milestones, "Changed Status to " + newstatus.ToString().SplitCamelCase());
  1009. Refresh();
  1010. }
  1011. private void EditMileStones(CoreRow[] rows)
  1012. {
  1013. var ids = rows.Select(r => r.Get<JobDocumentSetMileStone, Guid>(x => x.ID)).ToArray();
  1014. var milestones = new Client<JobDocumentSetMileStone>().Query(
  1015. new Filter<JobDocumentSetMileStone>(x => x.ID).InList(ids)
  1016. ).Rows.Select(r=>r.ToObject<JobDocumentSetMileStone>()).ToArray();
  1017. var grid = new JobDocumentSetMileStoneGrid();
  1018. if (grid.EditItems(milestones))
  1019. Refresh();
  1020. }
  1021. private void DeleteMileStone(CoreRow[] rows)
  1022. {
  1023. var milestones = rows.Select(r=>r.ToObject<JobDocumentSetMileStone>()).ToArray();
  1024. using (new WaitCursor())
  1025. new Client<JobDocumentSetMileStone>().Delete(milestones,"Deleted by User");
  1026. Refresh();
  1027. }
  1028. private void TreeGrid_OnCellToolTipOpening(object? sender, TreeGridCellToolTipOpeningEventArgs e)
  1029. {
  1030. var column = e.Column.MappingName.Replace("Blocks[","").Replace("]","");
  1031. var data = (e.Record as DocumentSetNode).Blocks[column];
  1032. if (String.IsNullOrWhiteSpace(data))
  1033. return;
  1034. var block = Serialization.Deserialize<JobDocumentSetMileStoneBlock>(data.ToString());
  1035. Guid id = block.ID;
  1036. TextBlock text = new TextBlock();
  1037. if (!String.IsNullOrWhiteSpace(block.Notes))
  1038. {
  1039. text.Inlines.Add(new Run("Milestone Notes\n") { FontWeight = FontWeights.Bold, TextDecorations = TextDecorations.Underline });
  1040. text.Inlines.Add(new Run(block.Notes.Replace("=","").Replace("\n\n","\n")) { FontStyle = FontStyles.Italic });
  1041. }
  1042. if (block.Attachments > 0)
  1043. {
  1044. if (!String.IsNullOrWhiteSpace(block.Notes))
  1045. text.Inlines.Add(new Run("\n\n"));
  1046. text.Inlines.Add(new Run("Uploaded Files") { FontWeight = FontWeights.Bold, TextDecorations = TextDecorations.Underline });
  1047. var files = new Client<JobDocumentSetMileStoneFile>().Query(
  1048. new Filter<JobDocumentSetMileStoneFile>(x => x.EntityLink.ID).IsEqualTo(block.ID),
  1049. new Columns<JobDocumentSetMileStoneFile>(x => x.DocumentLink.FileName),
  1050. new SortOrder<JobDocumentSetMileStoneFile>(x => x.DocumentLink.FileName)
  1051. );
  1052. foreach (var row in files.Rows)
  1053. text.Inlines.Add(new Run("\n"+row.Get<JobDocumentSetMileStoneFile,String>(c=>c.DocumentLink.FileName)) { FontStyle = FontStyles.Italic });
  1054. }
  1055. if (!text.Inlines.Any())
  1056. {
  1057. e.ToolTip.Template = null;
  1058. return;
  1059. }
  1060. e.ToolTip.Template = TemplateGenerator.CreateControlTemplate(
  1061. typeof(System.Windows.Controls.ToolTip),
  1062. () =>
  1063. {
  1064. var border = new Border
  1065. {
  1066. BorderBrush = new SolidColorBrush(Colors.Gray),
  1067. BorderThickness = new Thickness(0.75),
  1068. CornerRadius = new CornerRadius(5),
  1069. Background = new SolidColorBrush(Colors.LightYellow),
  1070. Padding = new Thickness(5),
  1071. Child = text
  1072. };
  1073. return border;
  1074. }
  1075. );
  1076. }
  1077. #endregion
  1078. #region Button Bar Actions
  1079. private void AddTypes(MenuItem parent, Action<Guid> addfunction)
  1080. {
  1081. if(_types.Count == 0)
  1082. {
  1083. MenuItem item = new MenuItem() { Header = "No Document Milestones", IsEnabled = false };
  1084. parent.Items.Add(item);
  1085. }
  1086. else
  1087. {
  1088. foreach (var type in _types.Keys)
  1089. {
  1090. MenuItem item = new MenuItem() { Header = _types[type].Description, Tag = type };
  1091. item.Click += (o, e) => addfunction(type);
  1092. parent.Items.Add(item);
  1093. }
  1094. }
  1095. }
  1096. private void Add_OnClick(object sender, RoutedEventArgs e)
  1097. {
  1098. if (FolderIDs?.Any() != true)
  1099. {
  1100. MessageBox.Show("Please choose a Folder first!");
  1101. return;
  1102. }
  1103. else if(FolderIDs.First() == CoreUtils.FullGuid)
  1104. {
  1105. MessageBox.Show("Cannot add items to this folder.");
  1106. return;
  1107. }
  1108. ContextMenu menu = new ContextMenu();
  1109. var onetoone = new MenuItem() { Header = "Add Individual Files" };
  1110. AddTypes(onetoone, AddOneToOneFiles);
  1111. menu.Items.Add(onetoone);
  1112. var manytoone = new MenuItem() { Header = "Add Sets of Files" };
  1113. AddTypes(manytoone, AddManyToOneFiles);
  1114. menu.Items.Add(manytoone);
  1115. menu.Items.Add(new Separator());
  1116. var manual = new MenuItem() { Header = "Add Document Set Manually" };
  1117. manual.Click += (o, e) => { AddDocumentSet(); };
  1118. menu.Items.Add(manual);
  1119. menu.IsOpen = true;
  1120. }
  1121. private void AddOneToOneFiles(Guid type)
  1122. {
  1123. Guid folderid = FolderIDs?.FirstOrDefault() ?? Guid.Empty;
  1124. if (!SelectFiles(out String[] filenames))
  1125. return;
  1126. Progress.ShowModal("Preparing Upload", (progress) =>
  1127. {
  1128. Dictionary<String, Tuple<Document, JobDocumentSet, JobDocumentSetMileStone, JobDocumentSetMileStoneFile>> map =
  1129. new Dictionary<string, Tuple<Document, JobDocumentSet, JobDocumentSetMileStone, JobDocumentSetMileStoneFile>>();
  1130. foreach (var filename in filenames)
  1131. {
  1132. var data = File.ReadAllBytes(filename);
  1133. Document document = new Document()
  1134. {
  1135. FileName = Path.GetFileName(filename).ToLower(),
  1136. Data = data,
  1137. CRC = CoreUtils.CalculateCRC(data),
  1138. TimeStamp = new FileInfo(filename).LastWriteTime
  1139. };
  1140. JobDocumentSet set = new JobDocumentSet();
  1141. set.Job.ID = JobID;
  1142. set.Folder.ID = folderid;
  1143. set.Code = Path.GetFileNameWithoutExtension(filename).ToUpper();
  1144. set.Description = Path.GetFileNameWithoutExtension(filename).ToUpper();
  1145. set.Discipline.ID = DisciplineID;
  1146. set.Type.ID = TypeID;
  1147. set.Category.ID = CategoryID;
  1148. set.Area.ID = AreaID;
  1149. JobDocumentSetMileStone milestone = new JobDocumentSetMileStone();
  1150. milestone.Type.ID = type;
  1151. milestone.Status = JobDocumentSetMileStoneStatus.InProgress;
  1152. milestone.Due = DateTime.Today;
  1153. JobDocumentSetMileStoneFile file = new JobDocumentSetMileStoneFile();
  1154. map[filename] = new Tuple<Document, JobDocumentSet, JobDocumentSetMileStone, JobDocumentSetMileStoneFile>(
  1155. document,
  1156. set,
  1157. milestone,
  1158. file
  1159. );
  1160. }
  1161. progress.Report("Uploading Files");
  1162. var docs = map.Select(x => x.Value.Item1);
  1163. new Client<Document>().Save(docs, "Uploaded By File Selection");
  1164. progress.Report("Creating Document Sets");
  1165. var sets = map.Select(x => x.Value.Item2);
  1166. new Client<JobDocumentSet>().Save(sets, "Uploaded by File Selection");
  1167. progress.Report("Creating MileStones");
  1168. foreach (var key in map.Keys)
  1169. map[key].Item3.DocumentSet.ID = map[key].Item2.ID;
  1170. var milestones = map.Select(x => x.Value.Item3);
  1171. new Client<JobDocumentSetMileStone>().Save(milestones, "Uploaded by File Selection");
  1172. progress.Report("Linking Documents");
  1173. foreach (var key in map.Keys)
  1174. {
  1175. map[key].Item4.EntityLink.ID = map[key].Item3.ID;
  1176. map[key].Item4.DocumentLink.ID = map[key].Item1.ID;
  1177. }
  1178. var files = map.Select(x => x.Value.Item4);
  1179. new Client<JobDocumentSetMileStoneFile>().Save(files, "Uploaded by File Selection");
  1180. });
  1181. MessageBox.Show(String.Format("{0} Document Sets Created", filenames.Length));
  1182. Refresh();
  1183. }
  1184. private void AddManyToOneFiles(Guid type)
  1185. {
  1186. Guid folderid = FolderIDs?.FirstOrDefault() ?? Guid.Empty;
  1187. if (!SelectFiles(out String[] filenames))
  1188. return;
  1189. JobDocumentSet set = new JobDocumentSet();
  1190. set.Job.ID = JobID;
  1191. set.Folder.ID = folderid;
  1192. set.Discipline.ID = DisciplineID;
  1193. set.Type.ID = TypeID;
  1194. set.Category.ID = CategoryID;
  1195. set.Area.ID = AreaID;
  1196. var grid = new DynamicDataGrid<JobDocumentSet>();
  1197. grid.OnAfterSave += (form, items) =>
  1198. {
  1199. Progress.ShowModal("Creating MileStone", (progress) =>
  1200. {
  1201. JobDocumentSetMileStone milestone = new JobDocumentSetMileStone();
  1202. milestone.DocumentSet.ID = set.ID;
  1203. milestone.Type.ID = type;
  1204. milestone.Status = JobDocumentSetMileStoneStatus.InProgress;
  1205. milestone.Due = DateTime.Today;
  1206. new Client<JobDocumentSetMileStone>().Save(milestone, "Uploaded By File Selection");
  1207. progress.Report("Uploading Files");
  1208. List<Document> documents = new List<Document>();
  1209. foreach (var filename in filenames)
  1210. {
  1211. var data = File.ReadAllBytes(filename);
  1212. Document document = new Document()
  1213. {
  1214. FileName = Path.GetFileName(filename).ToLower(),
  1215. Data = data,
  1216. CRC = CoreUtils.CalculateCRC(data),
  1217. TimeStamp = new FileInfo(filename).LastWriteTime
  1218. };
  1219. documents.Add(document);
  1220. new Client<Document>().Save(documents, "Uploaded by File Selection");
  1221. }
  1222. progress.Report("Creating File Links");
  1223. List<JobDocumentSetMileStoneFile> files = new List<JobDocumentSetMileStoneFile>();
  1224. foreach (var document in documents)
  1225. {
  1226. JobDocumentSetMileStoneFile file = new JobDocumentSetMileStoneFile();
  1227. file.EntityLink.ID = milestone.ID;
  1228. file.DocumentLink.ID = document.ID;
  1229. files.Add(file);
  1230. }
  1231. new Client<JobDocumentSetMileStoneFile>().Save(files, "Uploaded by File Selection");
  1232. });
  1233. };
  1234. if (grid.EditItems(new[] { set }))
  1235. {
  1236. MessageBox.Show(String.Format("{0} files uploaded", filenames.Length));
  1237. Refresh();
  1238. }
  1239. }
  1240. private void AddDocumentSet()
  1241. {
  1242. Guid folderid = FolderIDs?.FirstOrDefault() ?? Guid.Empty;
  1243. JobDocumentSet set = new JobDocumentSet();
  1244. set.Job.ID = JobID;
  1245. set.Folder.ID = folderid;
  1246. set.Discipline.ID = DisciplineID;
  1247. set.Type.ID = TypeID;
  1248. set.Category.ID = CategoryID;
  1249. set.Area.ID = AreaID;
  1250. var grid = new DynamicDataGrid<JobDocumentSet>();
  1251. if (grid.EditItems(new[] { set }))
  1252. Refresh();
  1253. }
  1254. private void Edit_OnClick(object sender, RoutedEventArgs e)
  1255. {
  1256. if (treeGrid.SelectedItem == null)
  1257. {
  1258. MessageBox.Show("Please choose a Document Set first");
  1259. return;
  1260. }
  1261. Guid[] setIDs = treeGrid.SelectedItems.Select(x => (x as DocumentSetNode).ID).ToArray();
  1262. EditDocumentSets(setIDs);
  1263. }
  1264. private void EditDocumentSets(Guid[] setIDs)
  1265. {
  1266. var sets = new Client<JobDocumentSet>().Query(
  1267. new Filter<JobDocumentSet>(x => x.ID).InList(setIDs)
  1268. ).Rows.Select(x => x.ToObject<JobDocumentSet>()).ToArray();
  1269. var grid = new DynamicDataGrid<JobDocumentSet>();
  1270. // grid.OnCustomiseEditor += (form, items, column, editor) =>
  1271. // {
  1272. // if (String.Equals(column.ColumnName, "Discipline.ID"))
  1273. // editor.Editable = DisciplineVisible ? Editable.Enabled : Editable.Hidden;
  1274. // if (String.Equals(column.ColumnName, "Type.ID"))
  1275. // editor.Editable = TypeVisible ? Editable.Enabled : Editable.Hidden;
  1276. // if (String.Equals(column.ColumnName, "Category.ID"))
  1277. // editor.Editable = CategoryVisible ? Editable.Enabled : Editable.Hidden;
  1278. // if (String.Equals(column.ColumnName, "Area.ID"))
  1279. // editor.Editable = AreaVisible ? Editable.Enabled : Editable.Hidden;
  1280. // };
  1281. if (grid.EditItems(sets))
  1282. UpdateNodes(sets);
  1283. }
  1284. private void UpdateNodes(IEnumerable<JobDocumentSet> sets)
  1285. {
  1286. if (_documentsets == null)
  1287. return;
  1288. foreach (var set in sets)
  1289. {
  1290. var node = _documentsets.GetNode(set.ID);
  1291. if (node != null)
  1292. {
  1293. var tags = new List<String>()
  1294. {
  1295. set.Discipline.Description,
  1296. set.Type.Description,
  1297. set.Category.Description,
  1298. set.Area.Description
  1299. }.Where(x=>!String.IsNullOrWhiteSpace(x)).Distinct().ToArray();
  1300. JobDocumentSetDescriptionBlock desc = new JobDocumentSetDescriptionBlock(
  1301. set.ID, set.Code, set.Description, tags);
  1302. node.Description = Serialization.Serialize(desc);
  1303. JobDocumentSetDetailsBlock dets = new JobDocumentSetDetailsBlock()
  1304. {
  1305. ID = set.ID,
  1306. Date = set.Date,
  1307. Size = set.Size,
  1308. Scale = set.Scale,
  1309. Employee = set.Employee.Name
  1310. };
  1311. node.Details = Serialization.Serialize(dets);
  1312. }
  1313. }
  1314. }
  1315. private void HideRejected_OnClick(object sender, RoutedEventArgs e)
  1316. {
  1317. _hidesuperceded = !_hidesuperceded;
  1318. HideSupercededLabel.Content = _hidesuperceded ? "Show All" : "Last Only";
  1319. Refresh();
  1320. }
  1321. private void Delete_OnClick(object sender, RoutedEventArgs e)
  1322. {
  1323. if ((treeGrid.SelectedItems == null) || !treeGrid.SelectedItems.Any())
  1324. {
  1325. MessageBox.Show("Please choose a Document Set first");
  1326. return;
  1327. }
  1328. if (MessageBox.Show(
  1329. "Are you sure you wish to delete the selected Document Sets?",
  1330. "Confirm Delete",
  1331. MessageBoxButton.YesNo
  1332. ) != MessageBoxResult.Yes)
  1333. return;
  1334. List<JobDocumentSet> updates = new List<JobDocumentSet>();
  1335. List<DocumentSetNode> orphans = new List<DocumentSetNode>();
  1336. var items = treeGrid.SelectedItems.Select(x => (DocumentSetNode)x).ToArray();
  1337. foreach (DocumentSetNode item in items)
  1338. {
  1339. var children = item.Children.Where(x => !items.Contains(x));
  1340. if (children.Any())
  1341. orphans.AddRange(children);
  1342. }
  1343. if (orphans.Any())
  1344. {
  1345. var confirm = MessageBox.Show(
  1346. "These Document Sets contain children!\nDo you wish to delete these as well?",
  1347. "Delete Children",
  1348. MessageBoxButton.YesNoCancel
  1349. );
  1350. if (confirm == MessageBoxResult.Cancel)
  1351. return;
  1352. if (confirm == MessageBoxResult.No)
  1353. {
  1354. foreach (var orphan in orphans)
  1355. {
  1356. var update = new JobDocumentSet();
  1357. update.ID = orphan.ID;
  1358. update.Parent.ID = Guid.Empty;
  1359. updates.Add(update);
  1360. }
  1361. return;
  1362. }
  1363. }
  1364. Progress.ShowModal("Deleting Document Set",(progress) =>
  1365. {
  1366. if (updates.Any())
  1367. new Client<JobDocumentSet>().Save(updates, "Parent Document Deleted");
  1368. var deletes = items.Select(x=>new JobDocumentSet() { ID = x.ID }).ToArray();
  1369. new Client<JobDocumentSet>().Delete(deletes, "Deleted By User");
  1370. });
  1371. Refresh();
  1372. }
  1373. #endregion
  1374. private void FlatList_OnClick(object sender, RoutedEventArgs e)
  1375. {
  1376. _flatlist = !_flatlist;
  1377. FlatListLabel.Content = _flatlist ? "Tree View" : "Flat List";
  1378. Refresh();
  1379. }
  1380. private void IncludeRetired_OnClick(object sender, RoutedEventArgs e)
  1381. {
  1382. _includeretired = !_includeretired;
  1383. FlatListLabel.Content = _includeretired ? "Active Only" : "Include Retired";
  1384. Refresh();
  1385. }
  1386. private void TreeGrid_OnSelectionChanged(object? sender, GridSelectionChangedEventArgs e)
  1387. {
  1388. //var treeColumn = treeGrid.Columns[e.CurrentRowColumnIndex.ColumnIndex];
  1389. //var column = treeColumn.MappingName.Replace("Blocks[","").Replace("]","");
  1390. // var column = e.Column.MappingName.Replace("Blocks[","").Replace("]","");
  1391. // var data = (e.Record as DocumentSetNode).Blocks[column];
  1392. // if (String.IsNullOrWhiteSpace(data))
  1393. // return;
  1394. //
  1395. // var block = Serialization.Deserialize<JobDocumentSetMileStoneBlock>(data.ToString());
  1396. // Guid id = block.ID;
  1397. }
  1398. private void TreeGrid_OnCurrentCellActivated(object? sender, CurrentCellActivatedEventArgs e)
  1399. {
  1400. var node = treeGrid.CurrentItem as DocumentSetNode;
  1401. if (node == null)
  1402. return;
  1403. var treeColumn = treeGrid.Columns[e.CurrentRowColumnIndex.ColumnIndex];
  1404. var column = treeColumn.MappingName.Replace("Blocks[","").Replace("]","");
  1405. if (!node.Blocks.ContainsKey(column))
  1406. MileStoneSelected(null);
  1407. else
  1408. {
  1409. var block = Serialization.Deserialize<JobDocumentSetMileStoneBlock>(node.Blocks[column]);
  1410. MileStoneSelected?.Invoke(block);
  1411. }
  1412. }
  1413. private void TreeGrid_OnCellDoubleTapped(object? sender, TreeGridCellDoubleTappedEventArgs e)
  1414. {
  1415. var set = e.Record as DocumentSetNode;
  1416. if (set != null)
  1417. EditDocumentSets(new Guid[] { set.ID });
  1418. }
  1419. }
  1420. }