diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ETriggerType.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ETriggerType.cs index 70a3151..a4523f2 100644 --- a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ETriggerType.cs +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ETriggerType.cs @@ -5,5 +5,6 @@ public enum ETriggerType HttpRequest, RecurringTimer, WalletInteraction, - ManualTrigger + ManualTrigger, + Form } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ParameterType.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ParameterType.cs index 12ff1f8..a6025a3 100644 --- a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ParameterType.cs +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/ParameterType.cs @@ -4,5 +4,6 @@ public enum ParameterType { String, Number, - Boolean + Boolean, + Date } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInput.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInput.cs index 50bccc9..b3e560d 100644 --- a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInput.cs +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInput.cs @@ -1,11 +1,11 @@ using System.Text.Json.Serialization; namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers; - [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] [JsonDerivedType(typeof(TriggerInputHttpRequest), typeDiscriminator: "incomingRequest")] [JsonDerivedType(typeof(TriggerInputRecurringTimer), typeDiscriminator: "recurringTimer")] [JsonDerivedType(typeof(TriggerInputOnDemand), typeDiscriminator: "onDemand")] +[JsonDerivedType(typeof(TriggerInputForm), typeDiscriminator: "form")] public class TriggerInput { [JsonPropertyName("id")] public Guid Id { get; set; } diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputForm.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputForm.cs new file mode 100644 index 0000000..88cb931 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputForm.cs @@ -0,0 +1,12 @@ +// namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers; +// +// public class TriggerInputForm : TriggerInput +// { +// public TriggerInputForm() +// { +// Id = Guid.NewGuid(); +// Parameters = new Dictionary(); +// } +// +// public Dictionary Parameters { get; set; } +// } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputs.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputs.cs index 0f0bfd7..f071dc4 100644 --- a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputs.cs +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/Triggers/TriggerInputs.cs @@ -24,4 +24,16 @@ public class TriggerInputManual : TriggerInput [JsonPropertyName("requiredParameters")] public Dictionary RequiredParameters { get; set; } = new(); +} + +public class TriggerInputForm : TriggerInput +{ + [JsonPropertyName("parameters")] + public Dictionary Parameters { get; set; } + + public TriggerInputForm() + { + Id = Guid.NewGuid(); + Parameters = new Dictionary(); + } } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/WorkflowContext.cs b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/WorkflowContext.cs index 1c7b752..bc9a40e 100644 --- a/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/WorkflowContext.cs +++ b/Blocktrust.CredentialWorkflow.Core/Domain/ProcessFlow/WorkflowContext.cs @@ -1,6 +1,4 @@ -using Blocktrust.CredentialWorkflow.Core.Domain.Common; - -namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow; +namespace Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow; using Actions; diff --git a/Blocktrust.CredentialWorkflow.Core/Entities/Identity/ApplicationUser.cs b/Blocktrust.CredentialWorkflow.Core/Entities/Identity/ApplicationUser.cs index 116d19e..adbf063 100644 --- a/Blocktrust.CredentialWorkflow.Core/Entities/Identity/ApplicationUser.cs +++ b/Blocktrust.CredentialWorkflow.Core/Entities/Identity/ApplicationUser.cs @@ -1,14 +1,13 @@ -namespace Blocktrust.CredentialWorkflow.Core.Entities.Identity -{ - using Microsoft.AspNetCore.Identity; - using Tenant; + namespace Blocktrust.CredentialWorkflow.Core.Entities.Identity; - // Add profile data for application users by adding properties to the ApplicationUser class - public class ApplicationUser : IdentityUser - { - [PersonalData] public string? SomeOtherData { get; set; } + using Microsoft.AspNetCore.Identity; + using Tenant; - public Guid? TenantEntityId { get; set; } - public TenantEntity? TenantEntity { get; set; } = null!; - } -} \ No newline at end of file + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + [PersonalData] public string? SomeOtherData { get; set; } + + public Guid? TenantEntityId { get; set; } + public TenantEntity? TenantEntity { get; set; } = null!; + } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Core/Migrations/20250211153232_MigrationName.Designer.cs b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211153232_MigrationName.Designer.cs new file mode 100644 index 0000000..bd76d20 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211153232_MigrationName.Designer.cs @@ -0,0 +1,483 @@ +// +using System; +using Blocktrust.CredentialWorkflow.Core; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Blocktrust.CredentialWorkflow.Core.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250211153232_MigrationName")] + partial class MigrationName + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("SomeOtherData") + .HasColumnType("text"); + + b.Property("TenantEntityId") + .HasColumnType("uuid"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("TenantEntityId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Outcome.WorkflowOutcomeEntity", b => + { + b.Property("WorkflowOutcomeEntityId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionOutcomesJson") + .HasColumnType("text"); + + b.Property("EndedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExecutionContext") + .HasColumnType("text"); + + b.Property("StartedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowEntityId") + .HasColumnType("uuid"); + + b.Property("WorkflowOutcomeState") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("WorkflowOutcomeEntityId"); + + b.HasIndex("WorkflowEntityId"); + + b.ToTable("WorkflowOutcomeEntities"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.IssuingKeyEntity", b => + { + b.Property("IssuingKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Did") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("KeyType") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(true) + .HasColumnType("character varying(100)"); + + b.Property("PrivateKey") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("PublicKey") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("TenantEntityId") + .HasColumnType("uuid"); + + b.HasKey("IssuingKeyId"); + + b.HasIndex("TenantEntityId"); + + b.ToTable("IssuingKeys"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", b => + { + b.Property("TenantEntityId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(true) + .HasColumnType("character varying(100)"); + + b.HasKey("TenantEntityId"); + + b.ToTable("TenantEntities"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", b => + { + b.Property("WorkflowEntityId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRunable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)"); + + b.Property("ProcessFlowJson") + .IsUnicode(false) + .HasColumnType("text"); + + b.Property("TenantEntityId") + .HasColumnType("uuid"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowState") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("WorkflowEntityId"); + + b.HasIndex("TenantEntityId"); + + b.ToTable("WorkflowEntities"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", "TenantEntity") + .WithMany("ApplicationUsers") + .HasForeignKey("TenantEntityId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("TenantEntity"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Outcome.WorkflowOutcomeEntity", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", "WorkflowEntity") + .WithMany("WorkflowOutcomeEntities") + .HasForeignKey("WorkflowEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkflowEntity"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.IssuingKeyEntity", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", null) + .WithMany("IssuingKeys") + .HasForeignKey("TenantEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", "TenantEntity") + .WithMany("WorkflowEntities") + .HasForeignKey("TenantEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TenantEntity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", b => + { + b.Navigation("ApplicationUsers"); + + b.Navigation("IssuingKeys"); + + b.Navigation("WorkflowEntities"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", b => + { + b.Navigation("WorkflowOutcomeEntities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Blocktrust.CredentialWorkflow.Core/Migrations/20250211153232_MigrationName.cs b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211153232_MigrationName.cs new file mode 100644 index 0000000..ee740aa --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211153232_MigrationName.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Blocktrust.CredentialWorkflow.Core.Migrations +{ + /// + public partial class MigrationName : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Blocktrust.CredentialWorkflow.Core/Migrations/20250211154309_Neww.Designer.cs b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211154309_Neww.Designer.cs new file mode 100644 index 0000000..ddff2fb --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211154309_Neww.Designer.cs @@ -0,0 +1,483 @@ +// +using System; +using Blocktrust.CredentialWorkflow.Core; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Blocktrust.CredentialWorkflow.Core.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250211154309_Neww")] + partial class Neww + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("SomeOtherData") + .HasColumnType("text"); + + b.Property("TenantEntityId") + .HasColumnType("uuid"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("TenantEntityId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Outcome.WorkflowOutcomeEntity", b => + { + b.Property("WorkflowOutcomeEntityId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActionOutcomesJson") + .HasColumnType("text"); + + b.Property("EndedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("ExecutionContext") + .HasColumnType("text"); + + b.Property("StartedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowEntityId") + .HasColumnType("uuid"); + + b.Property("WorkflowOutcomeState") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("WorkflowOutcomeEntityId"); + + b.HasIndex("WorkflowEntityId"); + + b.ToTable("WorkflowOutcomeEntities"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.IssuingKeyEntity", b => + { + b.Property("IssuingKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Did") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("KeyType") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(true) + .HasColumnType("character varying(100)"); + + b.Property("PrivateKey") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("PublicKey") + .IsRequired() + .HasMaxLength(1000) + .IsUnicode(true) + .HasColumnType("character varying(1000)"); + + b.Property("TenantEntityId") + .HasColumnType("uuid"); + + b.HasKey("IssuingKeyId"); + + b.HasIndex("TenantEntityId"); + + b.ToTable("IssuingKeys"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", b => + { + b.Property("TenantEntityId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(true) + .HasColumnType("character varying(100)"); + + b.HasKey("TenantEntityId"); + + b.ToTable("TenantEntities"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", b => + { + b.Property("WorkflowEntityId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRunable") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnType("character varying(100)"); + + b.Property("ProcessFlowJson") + .IsUnicode(false) + .HasColumnType("text"); + + b.Property("TenantEntityId") + .HasColumnType("uuid"); + + b.Property("UpdatedUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowState") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("WorkflowEntityId"); + + b.HasIndex("TenantEntityId"); + + b.ToTable("WorkflowEntities"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", "TenantEntity") + .WithMany("ApplicationUsers") + .HasForeignKey("TenantEntityId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("TenantEntity"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Outcome.WorkflowOutcomeEntity", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", "WorkflowEntity") + .WithMany("WorkflowOutcomeEntities") + .HasForeignKey("WorkflowEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WorkflowEntity"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.IssuingKeyEntity", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", null) + .WithMany("IssuingKeys") + .HasForeignKey("TenantEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", "TenantEntity") + .WithMany("WorkflowEntities") + .HasForeignKey("TenantEntityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TenantEntity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Blocktrust.CredentialWorkflow.Core.Entities.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Tenant.TenantEntity", b => + { + b.Navigation("ApplicationUsers"); + + b.Navigation("IssuingKeys"); + + b.Navigation("WorkflowEntities"); + }); + + modelBuilder.Entity("Blocktrust.CredentialWorkflow.Core.Entities.Workflow.WorkflowEntity", b => + { + b.Navigation("WorkflowOutcomeEntities"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Blocktrust.CredentialWorkflow.Core/Migrations/20250211154309_Neww.cs b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211154309_Neww.cs new file mode 100644 index 0000000..d2257ee --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Migrations/20250211154309_Neww.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Blocktrust.CredentialWorkflow.Core.Migrations +{ + /// + public partial class Neww : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Blocktrust.CredentialWorkflow.Core/Services/FormService.cs b/Blocktrust.CredentialWorkflow.Core/Services/FormService.cs new file mode 100644 index 0000000..d4c99b0 --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Core/Services/FormService.cs @@ -0,0 +1,84 @@ +using System.Text.Json; +using Blocktrust.CredentialWorkflow.Core.Commands.WorkflowOutcome.CreateWorkflowOutcome; +using Blocktrust.CredentialWorkflow.Core.Commands.Workflow.GetWorkflowById; +using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers; +using FluentResults; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace Blocktrust.CredentialWorkflow.Core.Services; + +public interface IFormService +{ + Task> ProcessFormSubmission(Guid workflowId, Dictionary formData); +} + +public class FormService : IFormService +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + private readonly IWorkflowQueue _workflowQueue; + + public FormService(IMediator mediator, ILogger logger, IWorkflowQueue workflowQueue) + { + _mediator = mediator; + _logger = logger; + _workflowQueue = workflowQueue; + } + + public async Task> ProcessFormSubmission(Guid workflowId, Dictionary formData) + { + try + { + // Get workflow + var workflowResult = await _mediator.Send(new GetWorkflowByIdRequest(workflowId)); + if (workflowResult.IsFailed) + { + return Result.Fail("Workflow not found"); + } + + var workflow = workflowResult.Value; + var trigger = workflow.ProcessFlow?.Triggers.FirstOrDefault().Value; + + if (trigger?.Type != ETriggerType.Form || !(trigger.Input is TriggerInputForm formTrigger)) + { + return Result.Fail("Invalid trigger type"); + } + + // Validate form data against parameters + foreach (var param in formTrigger.Parameters) + { + if (!formData.ContainsKey(param.Key)) + { + return Result.Fail($"Missing required parameter: {param.Key}"); + } + + // Type validation could be added here + } + + // Create execution context + var executionContext = JsonSerializer.Serialize(new + { + Type = "FormSubmission", + Data = formData + }); + + // Create workflow outcome + var outcomeResult = await _mediator.Send(new CreateWorkflowOutcomeRequest(workflowId, executionContext)); + if (outcomeResult.IsFailed) + { + return Result.Fail("Failed to create workflow outcome"); + } + + // Enqueue for processing + await _workflowQueue.EnqueueAsync(outcomeResult.Value); + + return Result.Ok(outcomeResult.Value); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing form submission for workflow {WorkflowId}", workflowId); + return Result.Fail("An error occurred while processing the form submission"); + } + } +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Blocktrust.CredentialWorkflow.Web.csproj b/Blocktrust.CredentialWorkflow.Web/Blocktrust.CredentialWorkflow.Web.csproj index 0c96312..979e44b 100644 --- a/Blocktrust.CredentialWorkflow.Web/Blocktrust.CredentialWorkflow.Web.csproj +++ b/Blocktrust.CredentialWorkflow.Web/Blocktrust.CredentialWorkflow.Web.csproj @@ -45,6 +45,7 @@ <_ContentIncludedByDefault Remove="Components\Features\Outcome\WorkflowOutcomeComponent.razor" /> <_ContentIncludedByDefault Remove="Components\Features\PropertyWindow\Outcome\WorkflowOutcomeComponent.razor" /> <_ContentIncludedByDefault Remove="Components\Layout\TopBarComponents\Breadcrumb.razor" /> + <_ContentIncludedByDefault Remove="Components\Features\HttpEndpointInfo\HttpEndpointInfo.razor" /> diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/HttpEndpointInfo/HttpEndpointInfo.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/HttpEndpointInfo/HttpEndpointInfo.razor deleted file mode 100644 index 9a4fbdd..0000000 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/HttpEndpointInfo/HttpEndpointInfo.razor +++ /dev/null @@ -1,175 +0,0 @@ -@using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers -@using Blocktrust.CredentialWorkflow.Web.Services -@using System.Web -@inject NavigationManager NavigationManager -@inject ClipboardService ClipboardService - -
-
-

HTTP Endpoint Reference

- -
- - @if (isExpanded) - { -
- @if (showCopiedMessage) - { -
- Copied to clipboard! -
- } - - -
-
- Endpoint Details - -
-
-
Method: @TriggerInput.Method
-
@DisplayUrl
-
-
- - -
-
- cURL Example - -
-
-
@CurlCommand
-
-
- - -
-
- Request Schema - -
-
-
@TruncatedJsonSchema
-
-
-
- } -
- -@code { - [Parameter] public Guid WorkflowId { get; set; } - [Parameter] public TriggerInputHttpRequest TriggerInput { get; set; } = null!; - - private bool showCopiedMessage; - private bool isExpanded = true; - - private string FullUrl => $"{NavigationManager.BaseUri.TrimEnd('/')}/api/workflow/{WorkflowId}"; - private string DisplayUrl => FullUrl.Length > 60 ? $"{FullUrl[..57]}..." : FullUrl; - - private string CurlCommand - { - get - { - var command = $"curl -X {TriggerInput.Method} \"{FullUrl}\""; - - var requiredParams = TriggerInput.Parameters.Where(p => p.Value.Required); - - // Add query parameters if GET request - if (TriggerInput.Method == "GET" && requiredParams.Any()) - { - var queryParams = requiredParams - .Select(p => $"{p.Key}={HttpUtility.UrlEncode($"example {p.Key}")}"); - command += $"?{string.Join("&", queryParams)}"; - } - - // Add headers and body for POST/PUT - if (TriggerInput.Method is "POST" or "PUT" && requiredParams.Any()) - { - command += " \\\n -H \"Content-Type: application/json\""; - - // Add example body with required parameters - var exampleBody = requiredParams - .ToDictionary( - p => p.Key, - p => $"example {p.Key}" - ); - - var jsonBody = System.Text.Json.JsonSerializer.Serialize(exampleBody, - new System.Text.Json.JsonSerializerOptions { - WriteIndented = true, - PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase - }); - command += $" \\\n -d '{jsonBody}'"; - } - - return command; - } - } - - private string JsonSchema - { - get - { - var schema = new - { - type = "object", - required = TriggerInput.Parameters - .Where(p => p.Value.Required) - .Select(p => p.Key) - .ToList(), - properties = TriggerInput.Parameters.ToDictionary( - p => p.Key, - p => new - { - type = p.Value.Type.ToString().ToLower(), - description = p.Value.Description ?? $"Parameter: {p.Key}", - required = p.Value.Required - } - ) - }; - - return System.Text.Json.JsonSerializer.Serialize(schema, - new System.Text.Json.JsonSerializerOptions { - WriteIndented = true, - PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase - }); - } - } - - private string TruncatedJsonSchema - { - get - { - var fullSchema = JsonSchema; - if (fullSchema.Length <= 300) return fullSchema; - return fullSchema[..300] + "\n ...\n}"; - } - } - private async Task CopyToClipboard(string text) - { - await ClipboardService.CopyTextToClipboard(text); - showCopiedMessage = true; - await InvokeAsync(async () => - { - await Task.Delay(2000); - showCopiedMessage = false; - StateHasChanged(); - }); - } -} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor index 793ddec..38501fc 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyEditor.razor @@ -12,27 +12,26 @@ @using Blocktrust.CredentialWorkflow.Web.Components.Features.Actions.Issuance @using Blocktrust.CredentialWorkflow.Web.Components.Features.Actions.Verification -
+
- - @if (Item is Trigger trigger) +@if (Item is Trigger trigger) +{ + @switch (trigger.Type) { - @switch (trigger.Type) - { - case ETriggerType.HttpRequest: - @if (trigger.Input is TriggerInputHttpRequest request) - { - - } - - break; - case ETriggerType.RecurringTimer: + case ETriggerType.HttpRequest: + @if (trigger.Input is TriggerInputHttpRequest request) + { + + } + break; + case ETriggerType.RecurringTimer: @if (trigger.Input is TriggerInputRecurringTimer recurringTimer) { + } + break; } } else if (Item is WorkflowAction action) @@ -153,7 +162,6 @@ } }
- @code { [Parameter] public object Item { get; set; } = null!; [Parameter] public string? Template { get; set; } @@ -161,4 +169,5 @@ [Parameter] public EventCallback OnChange { get; set; } [Parameter] public IEnumerable? TriggerParameters { get; set; } [Parameter] public IEnumerable? FlowItems { get; set; } + [Parameter] public Guid WorkflowId { get; set; } } \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor index 915fb6b..47437a2 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/PropertyWindow/PropertyWindow.razor @@ -53,6 +53,9 @@ + @@ -116,7 +119,8 @@ OnBack="@(() => SelectItem(null))" OnChange="@OnChange" TriggerParameters="@GetTriggerParameters()" - FlowItems="FlowItems"/> + FlowItems="FlowItems" + WorkflowId="@workflowId"/> } @if (_showError) @@ -140,6 +144,7 @@ [Parameter] public EventCallback OnItemCreated { get; set; } [Parameter] public EventCallback OnChange { get; set; } [Parameter] public List FlowItems { get; set; } = new(); + [Parameter] public Guid workflowId { get; set; } private string? _errorMessage; private bool _showError; @@ -404,6 +409,14 @@ RequiredParameters = new Dictionary() }; } + else if (triggerType == ETriggerType.Form) + { + trigger = new TriggerInputForm + { + Id = Guid.NewGuid(), + Parameters = new Dictionary() + }; + } else { throw new ArgumentException($"Trigger type {triggerType} is not implemented."); diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/FormTrigger.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/FormTrigger.razor new file mode 100644 index 0000000..5c0eb7a --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/FormTrigger.razor @@ -0,0 +1,183 @@ +@using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers +@using System.Text.RegularExpressions +@namespace Blocktrust.CredentialWorkflow.Web.Components.Features.Triggers + +
+
+ +
+
+
+

Form Parameters

+
+
+ +
+ @foreach (var param in formParameters) + { +
+ @if (!string.IsNullOrEmpty(param.ValidationError)) + { +

@param.ValidationError

+ } +
+ + + + + +
+
+ +
+
+ } +
+ + +
+ + +
+
+
Form URL
+ +
+
+ @FormUrl +
+
+
+
+ +@code { + [Parameter] public TriggerInputForm TriggerInput { get; set; } = null!; + [Parameter] public EventCallback OnChange { get; set; } + [Parameter] public Guid WorkflowId { get; set; } + [Inject] private NavigationManager NavigationManager { get; set; } = default!; + [Inject] private IJSRuntime JSRuntime { get; set; } = default!; + private List formParameters = new(); + private string FormUrl => $"{NavigationManager.BaseUri.TrimEnd('/')}/form/{WorkflowId}"; + + private class FormParameter + { + public string Name { get; set; } = ""; + public string Type { get; set; } = "string"; + public string? Description { get; set; } + public string ValidationError { get; set; } = ""; + } + + protected override void OnParametersSet() + { + if (WorkflowId != Guid.Empty) + { + InitializeParameters(); + } + } + + private void InitializeParameters() + { + formParameters.Clear(); + foreach (var param in TriggerInput.Parameters) + { + formParameters.Add(new FormParameter + { + Name = param.Key, + Type = param.Value.Type.ToString().ToLower(), + Description = param.Value.Description + }); + } + } + + private async Task AddParameter() + { + var paramName = $"param{formParameters.Count + 1}"; + var param = new FormParameter + { + Name = paramName, + Type = "string", + Description = $"Parameter {paramName}" + }; + formParameters.Add(param); + await UpdateParameters(); + StateHasChanged(); + } + private async Task RemoveParameter(FormParameter param) + { + formParameters.Remove(param); + await UpdateParameters(); + StateHasChanged(); + } + + private async Task UpdateParameters() + { + // Clear and rebuild parameters + var existingKeys = TriggerInput.Parameters.Keys.ToList(); + foreach (var key in existingKeys) + { + TriggerInput.Parameters.Remove(key); + } + + // Validate parameter names + var nameValidationRegex = new Regex("^[a-zA-Z][a-zA-Z0-9]*$"); + foreach (var param in formParameters) + { + param.ValidationError = ""; + if (string.IsNullOrEmpty(param.Name)) + { + param.ValidationError = "Parameter name cannot be empty."; + continue; + } + + if (!nameValidationRegex.IsMatch(param.Name)) + { + param.ValidationError = "Parameter name must start with a letter and contain only letters and numbers."; + continue; + } + + TriggerInput.Parameters[param.Name] = new ParameterDefinition + { + Type = Enum.Parse(param.Type, true), + Description = param.Description ?? $"Enter {param.Name}", + Required = true // Making all form fields required by default + }; + } + + await OnChange.InvokeAsync(); + } + + private async Task CopyToClipboard(string text) + { + await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); + // Could add toast notification here + } +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/HttpRequestTrigger.razor b/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/HttpRequestTrigger.razor index 9aba12f..a2ff731 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/HttpRequestTrigger.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Features/Triggers/HttpRequestTrigger.razor @@ -1,22 +1,40 @@ @namespace Blocktrust.CredentialWorkflow.Web.Components.Features.Triggers @using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers @using Blocktrust.CredentialWorkflow.Core.Domain.Enums +@using Blocktrust.CredentialWorkflow.Web.Services @using System.Text.RegularExpressions +@using System.Web +@inject NavigationManager NavigationManager +@inject ClipboardService ClipboardService -
+
@if (showToast) { -
+
@toastMessage
} -
-
- +
+ +
+
+
Endpoint URL
+ +
+
+
+ @NavigationManager.BaseUri.TrimEnd('/')/api/workflow/ + @WorkflowId +
+
+
- - - + +
+ @foreach (var param in httpQueryParameters) + { +
+ @if (!string.IsNullOrEmpty(param.ValidationError)) + { +

@param.ValidationError

+ } +
+ + + + +
+ @if (!string.IsNullOrEmpty(param.Description)) + { +

@param.Description

+ }
- @if (!string.IsNullOrEmpty(param.Description)) - { -

@param.Description

- } -
- } + } +
-
+ + +
+
+
cURL Example
+ +
+
+
@CurlCommand
+ @if (!isExpanded && CurlCommand.Length > 200) + { +
+ + } + @if (isExpanded && CurlCommand.Length > 200) + { + + } +
+
- @code { [Parameter] public TriggerInputHttpRequest TriggerInput { get; set; } = null!; [Parameter] public EventCallback OnChange { get; set; } + [Parameter] public Guid WorkflowId { get; set; } - private ECredentialType selectedCredentialType = ECredentialType.W3cCredential; - private string schemaId = ""; private string? toastMessage; private bool showToast; + private bool isExpanded; private List httpQueryParameters = new(); - private bool areParametersValid = true; // Track overall validity, including delivery destination + private bool areParametersValid = true; private string deliveryValidationError = ""; + private string FullUrl => $"{NavigationManager.BaseUri.TrimEnd('/')}/api/workflow/{WorkflowId}"; + + private string CurlCommand + { + get + { + var command = $"curl -X {TriggerInput.Method} \"{FullUrl}\""; + + var requiredParams = TriggerInput.Parameters.Where(p => p.Value.Required); + + if (TriggerInput.Method == "GET" && requiredParams.Any()) + { + var queryParams = requiredParams + .Select(p => $"{p.Key}={HttpUtility.UrlEncode($"example {p.Key}")}"); + command += $"?{string.Join("&", queryParams)}"; + } + + if (TriggerInput.Method is "POST" or "PUT" && requiredParams.Any()) + { + command += " \\\n -H \"Content-Type: application/json\""; + + var exampleBody = requiredParams + .ToDictionary( + p => p.Key, + p => $"example {p.Key}" + ); + + var jsonBody = System.Text.Json.JsonSerializer.Serialize(exampleBody, + new System.Text.Json.JsonSerializerOptions { + WriteIndented = true, + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }); + command += $" \\\n -d '{jsonBody}'"; + } + + return command; + } + } + + private string JsonSchema + { + get + { + var schema = new + { + type = "object", + required = TriggerInput.Parameters + .Where(p => p.Value.Required) + .Select(p => p.Key) + .ToList(), + properties = TriggerInput.Parameters.ToDictionary( + p => p.Key, + p => new + { + type = p.Value.Type.ToString().ToLower(), + description = p.Value.Description ?? $"Parameter: {p.Key}", + required = p.Value.Required + } + ) + }; + + return System.Text.Json.JsonSerializer.Serialize(schema, + new System.Text.Json.JsonSerializerOptions { + WriteIndented = true, + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }); + } + } + private class HttpQueryParameters { public string Name { get; set; } = ""; @@ -102,13 +226,17 @@ public string ValidationError { get; set; } = ""; } - protected override void OnInitialized() + protected override void OnParametersSet() { - InitializeParameters(); + if (WorkflowId != Guid.Empty) + { + InitializeParameters(); + } } private void InitializeParameters() { + httpQueryParameters.Clear(); foreach (var param in TriggerInput.Parameters) { httpQueryParameters.Add(new HttpQueryParameters @@ -128,12 +256,17 @@ private async Task AddParameter() { - var param = new HttpQueryParameters(); + var paramName = $"param{httpQueryParameters.Count + 1}"; + var param = new HttpQueryParameters + { + Name = paramName, + Type = "string", + Required = false + }; httpQueryParameters.Add(param); await UpdateParameters(); StateHasChanged(); } - private async Task RemoveParameter(HttpQueryParameters param) { httpQueryParameters.Remove(param); @@ -152,7 +285,7 @@ TriggerInput.Parameters.Remove(key); } - var nameValidationRegex = new Regex("^[a-zA-Z]+$"); // Updated to allow uppercase + var nameValidationRegex = new Regex("^[a-zA-Z]+$"); foreach (var param in httpQueryParameters) { @@ -185,14 +318,14 @@ await OnChange.InvokeAsync(); } - private async Task ShowToast(string message) + private async Task CopyToClipboard(string text) { - toastMessage = message; + await ClipboardService.CopyTextToClipboard(text); + toastMessage = "Copied to clipboard!"; showToast = true; StateHasChanged(); - await Task.Delay(3000); + await Task.Delay(2000); showToast = false; StateHasChanged(); } - -} +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Pages/Designer.razor b/Blocktrust.CredentialWorkflow.Web/Components/Pages/Designer.razor index 2279d5d..01bb286 100644 --- a/Blocktrust.CredentialWorkflow.Web/Components/Pages/Designer.razor +++ b/Blocktrust.CredentialWorkflow.Web/Components/Pages/Designer.razor @@ -2,7 +2,6 @@ @using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow @using Blocktrust.CredentialWorkflow.Web.Components.Features @using Blocktrust.CredentialWorkflow.Web.Components.Features.PropertyWindow -@using Blocktrust.CredentialWorkflow.Web.Components.Features.HttpEndpointInfo @using Blocktrust.CredentialWorkflow.Web.Components.Layout @using Blocktrust.CredentialWorkflow.Web.Services @@ -26,7 +25,6 @@ @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Authorize()] @layout AppLayout - Blocktrust Credential Workflow Platform - Designer @if (AppStateService.IsInitialized) @@ -113,17 +111,7 @@
- - @if (currentWorkflow?.ProcessFlow?.Triggers.Any() == true && - currentWorkflow.ProcessFlow.Triggers.First().Value.Type == ETriggerType.HttpRequest) - { -
- -
- } - +
@if (showToast) @@ -144,7 +132,9 @@ SelectedItem="@selectedItem" OnItemCreated="HandleItemCreated" OnChange="HandleChange" - FlowItems="@flowItems"/> + FlowItems="@flowItems" + workflowId="@workflowId" + />
diff --git a/Blocktrust.CredentialWorkflow.Web/Components/Pages/DynamicForm.razor b/Blocktrust.CredentialWorkflow.Web/Components/Pages/DynamicForm.razor new file mode 100644 index 0000000..ab8884a --- /dev/null +++ b/Blocktrust.CredentialWorkflow.Web/Components/Pages/DynamicForm.razor @@ -0,0 +1,349 @@ +@page "/form/{WorkflowId:guid}" +@using Blocktrust.CredentialWorkflow.Core.Commands.Workflow.GetWorkflowById +@using Blocktrust.CredentialWorkflow.Core.Domain.ProcessFlow.Triggers +@using MediatR +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authorization +@using Blocktrust.CredentialWorkflow.Core.Services +@inject IMediator Mediator +@inject NavigationManager NavigationManager +@inject ILogger Logger +@inject IFormService FormService + +@attribute [AllowAnonymous] + +Workflow Form + +
+
+ @if (isLoading) + { +
+
+
+ } + else if (!string.IsNullOrEmpty(error)) + { +
+
+
+

Error

+

@error

+
+
+
+ } + else + { + @if (!string.IsNullOrEmpty(successMessage)) + { +
+
+
+

@successMessage

+
+
+
+ } + else + { + + + + @foreach (var param in parameters) + { +
+ + + @{ + var fieldModel = new FieldModel + { + Value = formModel[param.Name], + OnValueChanged = val => + { + formModel[param.Name] = val; + StateHasChanged(); + } + }; + } + + @switch (param.Type.ToLower()) + { + case "string": + case "email": + + break; + case "number": + + break; + case "boolean": + + break; + case "date": + + break; + } + +
+ } + +
+ +
+
+ } + } +
+
+ +@code { + [Parameter] public Guid WorkflowId { get; set; } + + private bool isLoading = true; + private string? error; + private string? successMessage; + private List<(string Name, string Type, string? Description)> parameters = new(); + private DynamicFormModel formModel = new(); + + private class FieldModel + { + private object _value; + private Action _onValueChanged; + + public object Value + { + get => _value; + set + { + _value = value; + _onValueChanged?.Invoke(value); + } + } + + public Action OnValueChanged + { + get => _onValueChanged; + set => _onValueChanged = value; + } + + public string StringValue + { + get => (string)(_value ?? string.Empty); + set => Value = value; + } + + public decimal DecimalValue + { + get => _value == null ? 0 : Convert.ToDecimal(_value); + set => Value = value; + } + + public bool BoolValue + { + get => _value != null && Convert.ToBoolean(_value); + set => Value = value; + } + + public DateTime DateValue + { + get => _value == null ? DateTime.Today : Convert.ToDateTime(_value); + set => Value = value; + } + } + + protected override async Task OnInitializedAsync() + { + try + { + var result = await Mediator.Send(new GetWorkflowByIdRequest(WorkflowId)); + if (result.IsFailed) + { + error = "Failed to load the form. The workflow may not exist."; + return; + } + + var workflow = result.Value; + if (workflow.ProcessFlow?.Triggers == null || !workflow.ProcessFlow.Triggers.Any()) + { + error = "This workflow does not have any triggers configured."; + return; + } + + var trigger = workflow.ProcessFlow.Triggers.First().Value; + if (trigger.Type != ETriggerType.Form) + { + error = "This workflow is not configured with a form trigger."; + return; + } + + if (trigger.Input is TriggerInputForm formTrigger) + { + foreach (var param in formTrigger.Parameters) + { + parameters.Add((param.Key, param.Value.Type.ToString().ToLower(), param.Value.Description)); + formModel[param.Key] = GetDefaultValueForParameter(param.Value.Type); + } + } + } + catch (Exception ex) + { + error = "An unexpected error occurred while loading the form."; + Logger.LogError(ex, "Error loading form for workflow {WorkflowId}", WorkflowId); + } + finally + { + isLoading = false; + } + } + + private object GetDefaultValueForParameter(ParameterType type) + { + return type switch + { + ParameterType.String => string.Empty, + ParameterType.Number => 0m, + ParameterType.Boolean => false, + ParameterType.Date => DateTime.Today, + _ => string.Empty + }; + } + + private async Task HandleSubmit() + { + try + { + isLoading = true; + StateHasChanged(); + + // Convert form data to object dictionary + var formData = formModel.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value + ); + + var result = await FormService.ProcessFormSubmission(WorkflowId, formData); + + if (result.IsSuccess) + { + successMessage = "Form submitted successfully!"; + } + else + { + error = result.Errors.First().Message; + } + } + catch (Exception ex) + { + error = "An error occurred while submitting the form."; + Logger.LogError(ex, "Error submitting form for workflow {WorkflowId}", WorkflowId); + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + public class DynamicFormModel : Dictionary + { + public new object this[string key] + { + get => ContainsKey(key) ? base[key] : null; + set => base[key] = value; + } + + // Custom validation attributes + public class RequiredPropertyAttribute : ValidationAttribute + { + private readonly string _propertyName; + + public RequiredPropertyAttribute(string propertyName) + { + _propertyName = propertyName; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var model = (DynamicFormModel)validationContext.ObjectInstance; + var propertyValue = model[_propertyName]; + + if (propertyValue == null || (propertyValue is string str && string.IsNullOrWhiteSpace(str))) + { + return new ValidationResult($"The {_propertyName} field is required."); + } + + return ValidationResult.Success; + } + } + + public class EmailValidationAttribute : ValidationAttribute + { + private readonly string _propertyName; + + public EmailValidationAttribute(string propertyName) + { + _propertyName = propertyName; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var model = (DynamicFormModel)validationContext.ObjectInstance; + var propertyValue = model[_propertyName]?.ToString(); + + if (string.IsNullOrEmpty(propertyValue)) + { + return ValidationResult.Success; + } + + try + { + var addr = new System.Net.Mail.MailAddress(propertyValue); + return addr.Address == propertyValue ? ValidationResult.Success + : new ValidationResult($"The {_propertyName} field is not a valid email address."); + } + catch + { + return new ValidationResult($"The {_propertyName} field is not a valid email address."); + } + } + } + + public class DateValidationAttribute : ValidationAttribute + { + private readonly string _propertyName; + + public DateValidationAttribute(string propertyName) + { + _propertyName = propertyName; + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var model = (DynamicFormModel)validationContext.ObjectInstance; + var propertyValue = model[_propertyName]?.ToString(); + + if (string.IsNullOrEmpty(propertyValue)) + { + return ValidationResult.Success; + } + + if (!DateTime.TryParse(propertyValue, out _)) + { + return new ValidationResult($"The {_propertyName} field must be a valid date."); + } + + return ValidationResult.Success; + } + } + } +} \ No newline at end of file diff --git a/Blocktrust.CredentialWorkflow.Web/Program.cs b/Blocktrust.CredentialWorkflow.Web/Program.cs index a68ed95..b6469ad 100644 --- a/Blocktrust.CredentialWorkflow.Web/Program.cs +++ b/Blocktrust.CredentialWorkflow.Web/Program.cs @@ -50,6 +50,9 @@ builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + // Configure strongly typed settings builder.Services.Configure( builder.Configuration.GetSection("AppSettings")); diff --git a/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css b/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css index 60da7e2..aa60cbb 100644 --- a/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css +++ b/Blocktrust.CredentialWorkflow.Web/wwwroot/app.css @@ -620,6 +620,10 @@ video { bottom: 0px; } +.bottom-1 { + bottom: 0.25rem; +} + .left-0 { left: 0px; } @@ -769,6 +773,10 @@ video { margin-top: 2rem; } +.ml-3 { + margin-left: 0.75rem; +} + .block { display: block; } @@ -825,6 +833,18 @@ video { height: 100vh; } +.h-4 { + height: 1rem; +} + +.max-h-24 { + max-height: 6rem; +} + +.max-h-none { + max-height: none; +} + .min-h-\[100px\] { min-height: 100px; } @@ -861,6 +881,10 @@ video { width: 66.666667%; } +.w-24 { + width: 6rem; +} + .w-3\/4 { width: 75%; } @@ -893,6 +917,10 @@ video { width: 100%; } +.w-4 { + width: 1rem; +} + .min-w-0 { min-width: 0px; } @@ -906,6 +934,10 @@ video { min-width: 100%; } +.max-w-2xl { + max-width: 42rem; +} + .max-w-4xl { max-width: 56rem; } @@ -1089,14 +1121,14 @@ video { border-color: rgb(226 232 240 / var(--tw-divide-opacity)); } -.overflow-auto { - overflow: auto; -} - .overflow-hidden { overflow: hidden; } +.overflow-scroll { + overflow: scroll; +} + .overflow-x-auto { overflow-x: auto; } @@ -1105,6 +1137,10 @@ video { overflow-y: auto; } +.overflow-x-hidden { + overflow-x: hidden; +} + .truncate { overflow: hidden; text-overflow: ellipsis; @@ -1181,10 +1217,6 @@ video { border-top-width: 2px; } -.border-r { - border-right-width: 1px; -} - .border-blue-500 { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); @@ -1240,6 +1272,20 @@ video { border-color: rgb(15 23 42 / var(--tw-border-opacity)); } +.border-green-400 { + --tw-border-opacity: 1; + border-color: rgb(74 222 128 / var(--tw-border-opacity)); +} + +.border-red-400 { + --tw-border-opacity: 1; + border-color: rgb(248 113 113 / var(--tw-border-opacity)); +} + +.border-transparent { + border-color: transparent; +} + .bg-black { --tw-bg-opacity: 1; background-color: rgb(0 0 0 / var(--tw-bg-opacity)); @@ -1260,11 +1306,6 @@ video { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } -.bg-gray-200 { - --tw-bg-opacity: 1; - background-color: rgb(229 231 235 / var(--tw-bg-opacity)); -} - .bg-gray-50 { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); @@ -1365,6 +1406,11 @@ video { background-color: rgb(254 249 195 / var(--tw-bg-opacity)); } +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + .bg-green-50 { --tw-bg-opacity: 1; background-color: rgb(240 253 244 / var(--tw-bg-opacity)); @@ -1374,6 +1420,20 @@ video { --tw-bg-opacity: 0.5; } +.bg-gradient-to-t { + background-image: linear-gradient(to top, var(--tw-gradient-stops)); +} + +.from-white { + --tw-gradient-from: #fff var(--tw-gradient-from-position); + --tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.to-transparent { + --tw-gradient-to: transparent var(--tw-gradient-to-position); +} + .object-cover { -o-object-fit: cover; object-fit: cover; @@ -1403,6 +1463,10 @@ video { padding: 1.5rem; } +.p-8 { + padding: 2rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1468,6 +1532,11 @@ video { padding-bottom: 2rem; } +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + .pl-4 { padding-left: 1rem; } @@ -1484,6 +1553,10 @@ video { padding-top: 0.75rem; } +.pt-4 { + padding-top: 1rem; +} + .text-left { text-align: left; } @@ -1888,16 +1961,16 @@ a { background-color: rgb(71 85 105 / var(--tw-bg-opacity)); } -.hover\:bg-gray-200:hover { - --tw-bg-opacity: 1; - background-color: rgb(229 231 235 / var(--tw-bg-opacity)); -} - .hover\:text-blue-700:hover { --tw-text-opacity: 1; color: rgb(29 78 216 / var(--tw-text-opacity)); } +.hover\:text-blue-800:hover { + --tw-text-opacity: 1; + color: rgb(30 64 175 / var(--tw-text-opacity)); +} + .hover\:text-blue-900:hover { --tw-text-opacity: 1; color: rgb(30 58 138 / var(--tw-text-opacity)); @@ -1913,6 +1986,11 @@ a { color: rgb(209 213 219 / var(--tw-text-opacity)); } +.hover\:text-gray-700:hover { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + .hover\:text-gray-900:hover { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); @@ -1943,11 +2021,6 @@ a { color: rgb(51 65 85 / var(--tw-text-opacity)); } -.hover\:text-gray-700:hover { - --tw-text-opacity: 1; - color: rgb(55 65 81 / var(--tw-text-opacity)); -} - .hover\:opacity-80:hover { opacity: 0.8; } @@ -1957,6 +2030,11 @@ a { border-color: rgb(148 163 184 / var(--tw-border-opacity)); } +.focus\:border-slate-500:focus { + --tw-border-opacity: 1; + border-color: rgb(100 116 139 / var(--tw-border-opacity)); +} + .focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; @@ -1968,6 +2046,12 @@ a { box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); } +.focus\:ring-1:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + .focus\:ring-gray-300:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); @@ -2004,6 +2088,11 @@ a { .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .sm\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } } @media (min-width: 768px) {