feat: add user-based ticket endpoints

This commit is contained in:
Meik
2026-06-26 11:31:03 +02:00
parent 26eac54f94
commit 690ad4f615
720 changed files with 5232 additions and 2808 deletions

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.IO;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.TemplateWizard;
namespace Matrix42.Extensions.Scaffolder.Wizard
{
public class ExtensionProjectsWizard : IWizard
{
private DTE2 _dte;
private Project _placeholderProject;
private string _extensionNamespace = string.Empty;
private List<ProjectDefinition> _projects = new List<ProjectDefinition>();
private string _solutionDirectory = string.Empty;
public void RunStarted(
object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind,
object[] customParams)
{
_dte = automationObject as DTE2
?? throw new InvalidOperationException("DTE2 not available.");
using (var form = new WizardForm())
{
var result = form.ShowDialog();
if (result != System.Windows.Forms.DialogResult.OK)
throw new WizardBackoutException();
_extensionNamespace = form.ExtensionNamespace;
_projects = new List<ProjectDefinition>(form.Projects);
}
}
// 🔹 This is the method that was missing
public void BeforeOpeningFile(ProjectItem projectItem)
{
// We don't need to do anything before a file is opened.
// Leave empty on purpose.
}
public void ProjectFinishedGenerating(Project project)
{
_placeholderProject = project;
}
public void ProjectItemFinishedGenerating(ProjectItem projectItem)
{
}
public void RunFinished()
{
var solution = _dte.Solution;
_solutionDirectory = Path.GetDirectoryName(solution.FullName);
if (string.IsNullOrEmpty(_solutionDirectory))
return;
// Remove placeholder project from solution and disk
if (_placeholderProject != null)
{
var placeholderPath = _placeholderProject.FullName;
solution.Remove(_placeholderProject);
TryDeleteDirectorySafe(Path.GetDirectoryName(placeholderPath));
}
// Drop Directory.Build.props
var propsTemplatePath = Path.Combine(
GetExtensionInstallPath(),
"ProjectTemplates",
"Directory.Build.props.template");
if (File.Exists(propsTemplatePath))
{
var destProps = Path.Combine(_solutionDirectory, "Directory.Build.props");
if (!File.Exists(destProps))
File.Copy(propsTemplatePath, destProps, overwrite: false);
}
foreach (var proj in _projects)
{
CreateProjectFromDefinition(solution, proj);
}
}
public bool ShouldAddProjectItem(string filePath) => true;
private void CreateProjectFromDefinition(Solution solution, ProjectDefinition def)
{
var projectName = def.GetProjectName(_extensionNamespace);
var projectNamespace = def.GetNamespace(_extensionNamespace);
var projectDir = Path.Combine(_solutionDirectory, projectName);
Directory.CreateDirectory(projectDir);
var csprojPath = Path.Combine(projectDir, projectName + ".csproj");
var runtimeIds = def.GetRuntimeIdentifiers();
var tfm = "net8.0"; // default target framework
var csprojContent = GenerateCsproj(def.Type, tfm, runtimeIds, projectNamespace);
File.WriteAllText(csprojPath, csprojContent);
var codeFilePath = Path.Combine(
projectDir,
def.Type == ProjectType.WebApi ? "Program.cs" : "Class1.cs");
var codeContent = GenerateCodeFile(def.Type, projectNamespace);
File.WriteAllText(codeFilePath, codeContent);
solution.AddFromFile(csprojPath);
}
private string GenerateCsproj(
ProjectType type,
string tfm,
string runtimeIds,
string rootNamespace)
{
var outputType = type == ProjectType.WebApi ? "Exe" : "Library";
var sdk = type == ProjectType.WebApi ? "Microsoft.NET.Sdk.Web" : "Microsoft.NET.Sdk";
var ridBlock = string.IsNullOrWhiteSpace(runtimeIds)
? string.Empty
: $" <RuntimeIdentifiers>{runtimeIds}</RuntimeIdentifiers>{Environment.NewLine}";
return
$@"<Project Sdk=""{sdk}"">
<PropertyGroup>
<TargetFramework>{tfm}</TargetFramework>
<OutputType>{outputType}</OutputType>
<RootNamespace>{rootNamespace}</RootNamespace>
</PropertyGroup>
<PropertyGroup>
{ridBlock} </PropertyGroup>
<ItemGroup>
<PackageReference Include=""Matrix42.Extensions.SDK"" Version=""1.0.0"" />
</ItemGroup>
</Project>
";
}
private string GenerateCodeFile(ProjectType type, string ns)
{
switch (type)
{
case ProjectType.WebApi:
return
$@"using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Matrix42.Extensions.SDK;
namespace {ns}
{{
public class Program
{{
public static void Main(string[] args)
{{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// TODO: configure Matrix42.Extensions.SDK services
var app = builder.Build();
app.MapControllers();
app.Run();
}}
}}
}}";
case ProjectType.Contracts:
return
$@"namespace {ns}
{{
// Place your contracts (DTOs, interfaces) here
public class SampleContract
{{
public string Id {{ get; set; }} = string.Empty;
}}
}}";
case ProjectType.BizLogic:
return
$@"using Matrix42.Extensions.SDK;
namespace {ns}
{{
// Place your business logic services here
public class SampleService
{{
public string SayHello() => ""Hello from {ns}"";
}}
}}";
case ProjectType.Custom:
default:
return
$@"namespace {ns}
{{
// Custom project for your extension
public class CustomEntry
{{
public string Name {{ get; set; }} = ""Custom"";
}}
}}";
}
}
private string GetExtensionInstallPath()
{
var asmPath = typeof(ExtensionProjectsWizard).Assembly.Location;
return Path.GetDirectoryName(asmPath);
}
private void TryDeleteDirectorySafe(string path)
{
try
{
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
Directory.Delete(path, recursive: true);
}
catch
{
// ignore
}
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
namespace Matrix42.Extensions.Scaffolder.Wizard
{
public enum ProjectType
{
WebApi,
Contracts,
BizLogic,
Custom
}
public class ProjectDefinition
{
public ProjectType Type { get; set; }
public string Prefix { get; set; } = string.Empty; // custom for Custom type, otherwise derived
public RuntimeTargets Runtimes { get; set; }
public string EffectivePrefix
{
get
{
if (Type == ProjectType.Custom)
return Prefix;
switch (Type)
{
case ProjectType.WebApi:
return string.IsNullOrWhiteSpace(Prefix) ? "Services" : Prefix;
case ProjectType.Contracts:
return string.IsNullOrWhiteSpace(Prefix) ? "Contracts" : Prefix;
case ProjectType.BizLogic:
return string.IsNullOrWhiteSpace(Prefix) ? "BizLogic" : Prefix;
default:
return Prefix;
}
}
}
public string Display => $"{Type} - Prefix: {EffectivePrefix} - Runtimes: {Runtimes}";
public string GetProjectName(string extensionNamespace)
{
var pfx = EffectivePrefix?.Trim();
return string.IsNullOrWhiteSpace(pfx)
? extensionNamespace
: $"{extensionNamespace}.{pfx}";
}
public string GetNamespace(string extensionNamespace) => GetProjectName(extensionNamespace);
public string GetRuntimeIdentifiers()
{
if (Runtimes.HasFlag(RuntimeTargets.All))
return "win-x64;linux-x64";
var list = new List<string>();
if (Runtimes.HasFlag(RuntimeTargets.Win) || Runtimes.HasFlag(RuntimeTargets.WinCore))
list.Add("win-x64");
if (Runtimes.HasFlag(RuntimeTargets.LinuxCore))
list.Add("linux-x64");
return string.Join(";", list);
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Matrix42.Extensions.Scaffolder.Wizard
{
[System.Flags]
public enum RuntimeTargets
{
None = 0,
Win = 1,
WinCore = 2,
LinuxCore = 4,
All = 8
}
}

View File

@@ -0,0 +1,236 @@
namespace Matrix42.Extensions.Scaffolder.Wizard
{
partial class WizardForm
{
private System.ComponentModel.IContainer components = null;
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblExtensionNamespace;
private System.Windows.Forms.TextBox txtExtensionNamespace;
private System.Windows.Forms.Label lblHint;
private System.Windows.Forms.Label lblAddProjects;
private System.Windows.Forms.Label lblProjectType;
private System.Windows.Forms.ComboBox cboProjectType;
private System.Windows.Forms.Label lblProjectPrefix;
private System.Windows.Forms.TextBox txtProjectPrefix;
private System.Windows.Forms.Label lblRuntimes;
private System.Windows.Forms.CheckedListBox clbRuntimes;
private System.Windows.Forms.Button btnAddProject;
private System.Windows.Forms.ListBox lstProjects;
private System.Windows.Forms.Button btnRemoveSelected;
private System.Windows.Forms.Button btnOK;
private System.Windows.Forms.Button btnCancel;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
components.Dispose();
base.Dispose(disposing);
}
private void InitializeComponent()
{
this.lblTitle = new System.Windows.Forms.Label();
this.lblExtensionNamespace = new System.Windows.Forms.Label();
this.txtExtensionNamespace = new System.Windows.Forms.TextBox();
this.lblHint = new System.Windows.Forms.Label();
this.lblAddProjects = new System.Windows.Forms.Label();
this.lblProjectType = new System.Windows.Forms.Label();
this.cboProjectType = new System.Windows.Forms.ComboBox();
this.lblProjectPrefix = new System.Windows.Forms.Label();
this.txtProjectPrefix = new System.Windows.Forms.TextBox();
this.lblRuntimes = new System.Windows.Forms.Label();
this.clbRuntimes = new System.Windows.Forms.CheckedListBox();
this.btnAddProject = new System.Windows.Forms.Button();
this.lstProjects = new System.Windows.Forms.ListBox();
this.btnRemoveSelected = new System.Windows.Forms.Button();
this.btnOK = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// lblTitle
//
this.lblTitle.AutoSize = true;
this.lblTitle.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Bold);
this.lblTitle.Location = new System.Drawing.Point(12, 9);
this.lblTitle.Name = "lblTitle";
this.lblTitle.Size = new System.Drawing.Size(120, 20);
this.lblTitle.TabIndex = 0;
this.lblTitle.Text = "Projects Wizard";
//
// lblExtensionNamespace
//
this.lblExtensionNamespace.AutoSize = true;
this.lblExtensionNamespace.Location = new System.Drawing.Point(13, 44);
this.lblExtensionNamespace.Name = "lblExtensionNamespace";
this.lblExtensionNamespace.Size = new System.Drawing.Size(113, 15);
this.lblExtensionNamespace.TabIndex = 1;
this.lblExtensionNamespace.Text = "Extension Namespace";
//
// txtExtensionNamespace
//
this.txtExtensionNamespace.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.txtExtensionNamespace.Location = new System.Drawing.Point(16, 62);
this.txtExtensionNamespace.Name = "txtExtensionNamespace";
this.txtExtensionNamespace.Size = new System.Drawing.Size(444, 23);
this.txtExtensionNamespace.TabIndex = 2;
//
// lblHint
//
this.lblHint.AutoSize = true;
this.lblHint.Location = new System.Drawing.Point(466, 65);
this.lblHint.Name = "lblHint";
this.lblHint.Size = new System.Drawing.Size(121, 15);
this.lblHint.TabIndex = 3;
this.lblHint.Text = "aka Matrix42.MyExtension";
//
// lblAddProjects
//
this.lblAddProjects.AutoSize = true;
this.lblAddProjects.Location = new System.Drawing.Point(13, 103);
this.lblAddProjects.Name = "lblAddProjects";
this.lblAddProjects.Size = new System.Drawing.Size(71, 15);
this.lblAddProjects.TabIndex = 4;
this.lblAddProjects.Text = "Add Projects";
//
// lblProjectType
//
this.lblProjectType.AutoSize = true;
this.lblProjectType.Location = new System.Drawing.Point(13, 129);
this.lblProjectType.Name = "lblProjectType";
this.lblProjectType.Size = new System.Drawing.Size(70, 15);
this.lblProjectType.TabIndex = 5;
this.lblProjectType.Text = "Project Type";
//
// cboProjectType
//
this.cboProjectType.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cboProjectType.FormattingEnabled = true;
this.cboProjectType.Location = new System.Drawing.Point(16, 147);
this.cboProjectType.Name = "cboProjectType";
this.cboProjectType.Size = new System.Drawing.Size(150, 23);
this.cboProjectType.TabIndex = 6;
//
// lblProjectPrefix
//
this.lblProjectPrefix.AutoSize = true;
this.lblProjectPrefix.Location = new System.Drawing.Point(179, 129);
this.lblProjectPrefix.Name = "lblProjectPrefix";
this.lblProjectPrefix.Size = new System.Drawing.Size(75, 15);
this.lblProjectPrefix.TabIndex = 7;
this.lblProjectPrefix.Text = "Project Prefix";
//
// txtProjectPrefix
//
this.txtProjectPrefix.Location = new System.Drawing.Point(182, 147);
this.txtProjectPrefix.Name = "txtProjectPrefix";
this.txtProjectPrefix.Size = new System.Drawing.Size(150, 23);
this.txtProjectPrefix.TabIndex = 8;
//
// lblRuntimes
//
this.lblRuntimes.AutoSize = true;
this.lblRuntimes.Location = new System.Drawing.Point(345, 129);
this.lblRuntimes.Name = "lblRuntimes";
this.lblRuntimes.Size = new System.Drawing.Size(121, 15);
this.lblRuntimes.TabIndex = 9;
this.lblRuntimes.Text = "Runtimes (multiselect)";
//
// clbRuntimes
//
this.clbRuntimes.CheckOnClick = true;
this.clbRuntimes.FormattingEnabled = true;
this.clbRuntimes.Location = new System.Drawing.Point(348, 147);
this.clbRuntimes.Name = "clbRuntimes";
this.clbRuntimes.Size = new System.Drawing.Size(150, 76);
this.clbRuntimes.TabIndex = 10;
//
// btnAddProject
//
this.btnAddProject.Location = new System.Drawing.Point(504, 147);
this.btnAddProject.Name = "btnAddProject";
this.btnAddProject.Size = new System.Drawing.Size(75, 23);
this.btnAddProject.TabIndex = 11;
this.btnAddProject.Text = "Add";
this.btnAddProject.UseVisualStyleBackColor = true;
this.btnAddProject.Click += new System.EventHandler(this.btnAddProject_Click);
//
// lstProjects
//
this.lstProjects.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.lstProjects.FormattingEnabled = true;
this.lstProjects.ItemHeight = 15;
this.lstProjects.Location = new System.Drawing.Point(16, 236);
this.lstProjects.Name = "lstProjects";
this.lstProjects.Size = new System.Drawing.Size(563, 139);
this.lstProjects.TabIndex = 12;
//
// btnRemoveSelected
//
this.btnRemoveSelected.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnRemoveSelected.Location = new System.Drawing.Point(16, 385);
this.btnRemoveSelected.Name = "btnRemoveSelected";
this.btnRemoveSelected.Size = new System.Drawing.Size(120, 23);
this.btnRemoveSelected.TabIndex = 13;
this.btnRemoveSelected.Text = "Remove selected";
this.btnRemoveSelected.UseVisualStyleBackColor = true;
this.btnRemoveSelected.Click += new System.EventHandler(this.btnRemoveSelected_Click);
//
// btnOK
//
this.btnOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnOK.Location = new System.Drawing.Point(423, 385);
this.btnOK.Name = "btnOK";
this.btnOK.Size = new System.Drawing.Size(75, 23);
this.btnOK.TabIndex = 14;
this.btnOK.Text = "Create";
this.btnOK.UseVisualStyleBackColor = true;
this.btnOK.Click += new System.EventHandler(this.btnOK_Click);
//
// btnCancel
//
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnCancel.Location = new System.Drawing.Point(504, 385);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(75, 23);
this.btnCancel.TabIndex = 15;
this.btnCancel.Text = "Cancel";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// WizardForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(592, 420);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnOK);
this.Controls.Add(this.btnRemoveSelected);
this.Controls.Add(this.lstProjects);
this.Controls.Add(this.btnAddProject);
this.Controls.Add(this.clbRuntimes);
this.Controls.Add(this.lblRuntimes);
this.Controls.Add(this.txtProjectPrefix);
this.Controls.Add(this.lblProjectPrefix);
this.Controls.Add(this.cboProjectType);
this.Controls.Add(this.lblProjectType);
this.Controls.Add(this.lblAddProjects);
this.Controls.Add(this.lblHint);
this.Controls.Add(this.txtExtensionNamespace);
this.Controls.Add(this.lblExtensionNamespace);
this.Controls.Add(this.lblTitle);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "WizardForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Matrix42 Extension Solution Wizard";
this.Load += new System.EventHandler(this.WizardForm_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Matrix42.Extensions.Scaffolder.Wizard
{
public partial class WizardForm : Form
{
private readonly List<ProjectDefinition> _projects = new List<ProjectDefinition>();
public string ExtensionNamespace => txtExtensionNamespace.Text.Trim();
public IReadOnlyList<ProjectDefinition> Projects => _projects;
public WizardForm()
{
InitializeComponent();
cboProjectType.DataSource = Enum.GetValues(typeof(ProjectType));
clbRuntimes.Items.Add("Win");
clbRuntimes.Items.Add("WinCore");
clbRuntimes.Items.Add("LinuxCore");
clbRuntimes.Items.Add("ALL");
cboProjectType.SelectedIndexChanged += CboProjectType_SelectedIndexChanged;
}
private void WizardForm_Load(object sender, EventArgs e)
{
lblHint.Text = "aka Matrix42.MyExtension";
}
private void CboProjectType_SelectedIndexChanged(object sender, EventArgs e)
{
var type = (ProjectType)cboProjectType.SelectedItem;
switch (type)
{
case ProjectType.WebApi:
txtProjectPrefix.Text = "Services";
txtProjectPrefix.ReadOnly = false;
break;
case ProjectType.Contracts:
txtProjectPrefix.Text = "Contracts";
txtProjectPrefix.ReadOnly = false;
break;
case ProjectType.BizLogic:
txtProjectPrefix.Text = "BizLogic";
txtProjectPrefix.ReadOnly = false;
break;
case ProjectType.Custom:
txtProjectPrefix.Text = string.Empty;
txtProjectPrefix.ReadOnly = false;
break;
}
}
private void btnAddProject_Click(object sender, EventArgs e)
{
if (!ValidateProjectInputs())
return;
var def = new ProjectDefinition
{
Type = (ProjectType)cboProjectType.SelectedItem,
Prefix = txtProjectPrefix.Text.Trim(),
Runtimes = GetSelectedRuntimes()
};
_projects.Add(def);
lstProjects.Items.Add(def.Display);
txtProjectPrefix.Clear();
for (int i = 0; i < clbRuntimes.Items.Count; i++)
clbRuntimes.SetItemChecked(i, false);
}
private RuntimeTargets GetSelectedRuntimes()
{
RuntimeTargets result = RuntimeTargets.None;
foreach (var item in clbRuntimes.CheckedItems)
{
switch (item.ToString())
{
case "Win":
result |= RuntimeTargets.Win;
break;
case "WinCore":
result |= RuntimeTargets.WinCore;
break;
case "LinuxCore":
result |= RuntimeTargets.LinuxCore;
break;
case "ALL":
result |= RuntimeTargets.All;
break;
}
}
return result;
}
private bool ValidateProjectInputs()
{
if (cboProjectType.SelectedItem == null)
{
MessageBox.Show("Please select project type.");
return false;
}
var type = (ProjectType)cboProjectType.SelectedItem;
if (string.IsNullOrWhiteSpace(txtProjectPrefix.Text))
{
if (type == ProjectType.Custom)
{
MessageBox.Show("Project prefix is required for Custom project type.");
return false;
}
}
if (clbRuntimes.CheckedItems.Count == 0)
{
MessageBox.Show("Please select at least one runtime.");
return false;
}
return true;
}
private void btnRemoveSelected_Click(object sender, EventArgs e)
{
int idx = lstProjects.SelectedIndex;
if (idx < 0 || idx >= _projects.Count)
return;
_projects.RemoveAt(idx);
lstProjects.Items.RemoveAt(idx);
}
private void btnOK_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(ExtensionNamespace))
{
MessageBox.Show("Extension Namespace is required.");
return;
}
if (_projects.Count == 0)
{
MessageBox.Show("Please add at least one project.");
return;
}
DialogResult = DialogResult.OK;
Close();
}
private void btnCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}