Simplify release notes workflow
This commit is contained in:
2591
backend/src/Socialize.Api/Migrations/20260508030349_SimplifyReleaseUpdates.Designer.cs
generated
Normal file
2591
backend/src/Socialize.Api/Migrations/20260508030349_SimplifyReleaseUpdates.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class SimplifyReleaseUpdates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Audience",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Audience",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Body",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BuildVersion",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Category",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CommitRange",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DeploymentLabel",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Importance",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailAudience",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Audience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Body",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(8000)",
|
||||||
|
maxLength: 8000,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BuildVersion",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Category",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "CommitRange",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DeploymentLabel",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Importance",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ManualEmailAudience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Audience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "Audience");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2601
backend/src/Socialize.Api/Migrations/20260508031114_AddFrenchReleaseUpdateFields.Designer.cs
generated
Normal file
2601
backend/src/Socialize.Api/Migrations/20260508031114_AddFrenchReleaseUpdateFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddFrenchReleaseUpdateFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "TitleFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(160)",
|
||||||
|
maxLength: 160,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE "ReleaseUpdates"
|
||||||
|
SET "TitleFr" = "Title",
|
||||||
|
"SummaryFr" = "Summary"
|
||||||
|
WHERE "TitleFr" = '' AND "SummaryFr" = '';
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TitleFr",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2592
backend/src/Socialize.Api/Migrations/20260508034156_RemoveManualReleaseUpdateEmail.Designer.cs
generated
Normal file
2592
backend/src/Socialize.Api/Migrations/20260508034156_RemoveManualReleaseUpdateEmail.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class RemoveManualReleaseUpdateEmail : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailRecipientCount",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailSentAt",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailSentByUserId",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "ManualEmailRecipientCount",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "ManualEmailSentAt",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "ManualEmailSentByUserId",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2592
backend/src/Socialize.Api/Migrations/20260508034902_ExpandReleaseUpdateDescriptions.Designer.cs
generated
Normal file
2592
backend/src/Socialize.Api/Migrations/20260508034902_ExpandReleaseUpdateDescriptions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class ExpandReleaseUpdateDescriptions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(4000)",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(512)",
|
||||||
|
oldMaxLength: 512);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Summary",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(4000)",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(512)",
|
||||||
|
oldMaxLength: 512);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(4000)",
|
||||||
|
oldMaxLength: 4000);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Summary",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(4000)",
|
||||||
|
oldMaxLength: 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1980,28 +1980,6 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<DateTimeOffset?>("ArchivedAt")
|
b.Property<DateTimeOffset?>("ArchivedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Audience")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.Property<string>("Body")
|
|
||||||
.HasMaxLength(8000)
|
|
||||||
.HasColumnType("character varying(8000)");
|
|
||||||
|
|
||||||
b.Property<string>("BuildVersion")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("Category")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.Property<string>("CommitRange")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
@@ -2010,28 +1988,6 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<Guid>("CreatedByUserId")
|
b.Property<Guid>("CreatedByUserId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("DeploymentLabel")
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("Importance")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(32)
|
|
||||||
.HasColumnType("character varying(32)");
|
|
||||||
|
|
||||||
b.Property<string>("ManualEmailAudience")
|
|
||||||
.HasMaxLength(64)
|
|
||||||
.HasColumnType("character varying(64)");
|
|
||||||
|
|
||||||
b.Property<int?>("ManualEmailRecipientCount")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ManualEmailSentAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid?>("ManualEmailSentByUserId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("PublishedAt")
|
b.Property<DateTimeOffset?>("PublishedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@@ -2042,21 +1998,29 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.Property<string>("Summary")
|
b.Property<string>("Summary")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(512)
|
.HasMaxLength(4000)
|
||||||
.HasColumnType("character varying(512)");
|
.HasColumnType("character varying(4000)");
|
||||||
|
|
||||||
|
b.Property<string>("SummaryFr")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("character varying(4000)");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(160)
|
.HasMaxLength(160)
|
||||||
.HasColumnType("character varying(160)");
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
|
b.Property<string>("TitleFr")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("UpdatedAt")
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("Audience");
|
|
||||||
|
|
||||||
b.HasIndex("CreatedByUserId");
|
b.HasIndex("CreatedByUserId");
|
||||||
|
|
||||||
b.HasIndex("PublishedAt");
|
b.HasIndex("PublishedAt");
|
||||||
|
|||||||
@@ -5,23 +5,16 @@ namespace Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
|||||||
internal record ReleaseUpdateDto(
|
internal record ReleaseUpdateDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Title,
|
string Title,
|
||||||
string Summary,
|
string Description,
|
||||||
string? Body,
|
string TitleEn,
|
||||||
string Category,
|
string DescriptionEn,
|
||||||
string Importance,
|
string TitleFr,
|
||||||
string Audience,
|
string DescriptionFr,
|
||||||
string Status,
|
string Status,
|
||||||
string? DeploymentLabel,
|
|
||||||
string? BuildVersion,
|
|
||||||
string? CommitRange,
|
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
DateTimeOffset UpdatedAt,
|
DateTimeOffset UpdatedAt,
|
||||||
DateTimeOffset? PublishedAt,
|
DateTimeOffset? PublishedAt,
|
||||||
DateTimeOffset? ArchivedAt,
|
DateTimeOffset? ArchivedAt,
|
||||||
Guid? ManualEmailSentByUserId,
|
|
||||||
DateTimeOffset? ManualEmailSentAt,
|
|
||||||
string? ManualEmailAudience,
|
|
||||||
int? ManualEmailRecipientCount,
|
|
||||||
bool IsRead);
|
bool IsRead);
|
||||||
|
|
||||||
internal record ReleaseCommitDto(
|
internal record ReleaseCommitDto(
|
||||||
@@ -40,16 +33,13 @@ internal record ReleaseCommitDto(
|
|||||||
DateTimeOffset ImportedAt,
|
DateTimeOffset ImportedAt,
|
||||||
DateTimeOffset UpdatedAt);
|
DateTimeOffset UpdatedAt);
|
||||||
|
|
||||||
internal record ReleaseCommitImportResultDto(
|
internal record ReleaseCommitRefreshResultDto(
|
||||||
int ImportedCount,
|
int CreatedCount,
|
||||||
int UpdatedCount,
|
int UpdatedCount,
|
||||||
int SkippedCount,
|
int SkippedCount,
|
||||||
IReadOnlyCollection<ReleaseCommitDto> Commits);
|
IReadOnlyCollection<ReleaseCommitDto> Commits);
|
||||||
|
|
||||||
internal record ReleaseUpdateEmailSendResultDto(
|
internal record ReleaseCommitBulkLinkResultDto(int LinkedCount);
|
||||||
int RecipientCount,
|
|
||||||
DateTimeOffset SentAt,
|
|
||||||
bool TestMode);
|
|
||||||
|
|
||||||
internal record ReleaseUpdateUnreadSummaryDto(
|
internal record ReleaseUpdateUnreadSummaryDto(
|
||||||
int UnreadCount,
|
int UnreadCount,
|
||||||
@@ -64,22 +54,15 @@ internal static class ReleaseUpdateDtoMapper
|
|||||||
update.Id,
|
update.Id,
|
||||||
update.Title,
|
update.Title,
|
||||||
update.Summary,
|
update.Summary,
|
||||||
update.Body,
|
update.Title,
|
||||||
ToDisplayString(update.Category),
|
update.Summary,
|
||||||
update.Importance.ToString(),
|
update.TitleFr,
|
||||||
update.Audience.ToString(),
|
update.SummaryFr,
|
||||||
update.Status.ToString(),
|
update.Status.ToString(),
|
||||||
update.DeploymentLabel,
|
|
||||||
update.BuildVersion,
|
|
||||||
update.CommitRange,
|
|
||||||
update.CreatedAt,
|
update.CreatedAt,
|
||||||
update.UpdatedAt,
|
update.UpdatedAt,
|
||||||
update.PublishedAt,
|
update.PublishedAt,
|
||||||
update.ArchivedAt,
|
update.ArchivedAt,
|
||||||
update.ManualEmailSentByUserId,
|
|
||||||
update.ManualEmailSentAt,
|
|
||||||
update.ManualEmailAudience,
|
|
||||||
update.ManualEmailRecipientCount,
|
|
||||||
isRead);
|
isRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +85,4 @@ internal static class ReleaseUpdateDtoMapper
|
|||||||
commit.UpdatedAt);
|
commit.UpdatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ToDisplayString(ReleaseUpdateCategory category)
|
|
||||||
{
|
|
||||||
return category == ReleaseUpdateCategory.BreakingChange ? "Breaking Change" : category.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,12 @@ internal static class ReleaseCommunicationsModelConfiguration
|
|||||||
releaseUpdate.ToTable("ReleaseUpdates");
|
releaseUpdate.ToTable("ReleaseUpdates");
|
||||||
releaseUpdate.HasKey(x => x.Id);
|
releaseUpdate.HasKey(x => x.Id);
|
||||||
releaseUpdate.Property(x => x.Title).HasMaxLength(160).IsRequired();
|
releaseUpdate.Property(x => x.Title).HasMaxLength(160).IsRequired();
|
||||||
releaseUpdate.Property(x => x.Summary).HasMaxLength(512).IsRequired();
|
releaseUpdate.Property(x => x.Summary).HasMaxLength(4000).IsRequired();
|
||||||
releaseUpdate.Property(x => x.Body).HasMaxLength(8000);
|
releaseUpdate.Property(x => x.TitleFr).HasMaxLength(160).IsRequired();
|
||||||
releaseUpdate.Property(x => x.Category).HasConversion<string>().HasMaxLength(32).IsRequired();
|
releaseUpdate.Property(x => x.SummaryFr).HasMaxLength(4000).IsRequired();
|
||||||
releaseUpdate.Property(x => x.Importance).HasConversion<string>().HasMaxLength(32).IsRequired();
|
|
||||||
releaseUpdate.Property(x => x.Audience).HasConversion<string>().HasMaxLength(32).IsRequired();
|
|
||||||
releaseUpdate.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
|
releaseUpdate.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||||
releaseUpdate.Property(x => x.DeploymentLabel).HasMaxLength(128);
|
|
||||||
releaseUpdate.Property(x => x.BuildVersion).HasMaxLength(128);
|
|
||||||
releaseUpdate.Property(x => x.CommitRange).HasMaxLength(256);
|
|
||||||
releaseUpdate.Property(x => x.ManualEmailAudience).HasMaxLength(64);
|
|
||||||
releaseUpdate.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
releaseUpdate.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
releaseUpdate.HasIndex(x => x.Status);
|
releaseUpdate.HasIndex(x => x.Status);
|
||||||
releaseUpdate.HasIndex(x => x.Audience);
|
|
||||||
releaseUpdate.HasIndex(x => x.PublishedAt);
|
releaseUpdate.HasIndex(x => x.PublishedAt);
|
||||||
releaseUpdate.HasIndex(x => x.CreatedByUserId);
|
releaseUpdate.HasIndex(x => x.CreatedByUserId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,22 +5,13 @@ internal class ReleaseUpdate
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
public string Summary { get; set; } = string.Empty;
|
public string Summary { get; set; } = string.Empty;
|
||||||
public string? Body { get; set; }
|
public string TitleFr { get; set; } = string.Empty;
|
||||||
public ReleaseUpdateCategory Category { get; set; }
|
public string SummaryFr { get; set; } = string.Empty;
|
||||||
public ReleaseUpdateImportance Importance { get; set; }
|
|
||||||
public ReleaseUpdateAudience Audience { get; set; }
|
|
||||||
public ReleaseUpdateStatus Status { get; set; }
|
public ReleaseUpdateStatus Status { get; set; }
|
||||||
public string? DeploymentLabel { get; set; }
|
|
||||||
public string? BuildVersion { get; set; }
|
|
||||||
public string? CommitRange { get; set; }
|
|
||||||
public Guid CreatedByUserId { get; set; }
|
public Guid CreatedByUserId { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; set; }
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
public DateTimeOffset UpdatedAt { get; set; }
|
public DateTimeOffset UpdatedAt { get; set; }
|
||||||
public DateTimeOffset? PublishedAt { get; set; }
|
public DateTimeOffset? PublishedAt { get; set; }
|
||||||
public DateTimeOffset? ArchivedAt { get; set; }
|
public DateTimeOffset? ArchivedAt { get; set; }
|
||||||
public Guid? ManualEmailSentByUserId { get; set; }
|
|
||||||
public DateTimeOffset? ManualEmailSentAt { get; set; }
|
|
||||||
public string? ManualEmailAudience { get; set; }
|
|
||||||
public int? ManualEmailRecipientCount { get; set; }
|
|
||||||
public ICollection<ReleaseUpdateReadReceipt> ReadReceipts { get; } = new List<ReleaseUpdateReadReceipt>();
|
public ICollection<ReleaseUpdateReadReceipt> ReadReceipts { get; } = new List<ReleaseUpdateReadReceipt>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Data;
|
|
||||||
|
|
||||||
internal enum ReleaseUpdateAudience
|
|
||||||
{
|
|
||||||
Everyone,
|
|
||||||
OrganizationOwners,
|
|
||||||
Developers,
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Data;
|
|
||||||
|
|
||||||
internal enum ReleaseUpdateCategory
|
|
||||||
{
|
|
||||||
Feature,
|
|
||||||
Improvement,
|
|
||||||
Fix,
|
|
||||||
BreakingChange,
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Data;
|
|
||||||
|
|
||||||
internal enum ReleaseUpdateImportance
|
|
||||||
{
|
|
||||||
Normal,
|
|
||||||
Important,
|
|
||||||
}
|
|
||||||
@@ -4,35 +4,24 @@ using Socialize.Api.Infrastructure.Security;
|
|||||||
using Socialize.Api.Modules.Identity.Contracts;
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Services;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
||||||
|
|
||||||
internal record CreateDeveloperReleaseUpdateRequest(
|
internal record CreateDeveloperReleaseUpdateRequest(
|
||||||
string Title,
|
string TitleEn,
|
||||||
string Summary,
|
string DescriptionEn,
|
||||||
string? Body,
|
string TitleFr,
|
||||||
string Category,
|
string DescriptionFr);
|
||||||
string Importance,
|
|
||||||
string Audience,
|
|
||||||
string? DeploymentLabel,
|
|
||||||
string? BuildVersion,
|
|
||||||
string? CommitRange);
|
|
||||||
|
|
||||||
internal class CreateDeveloperReleaseUpdateRequestValidator
|
internal class CreateDeveloperReleaseUpdateRequestValidator
|
||||||
: Validator<CreateDeveloperReleaseUpdateRequest>
|
: Validator<CreateDeveloperReleaseUpdateRequest>
|
||||||
{
|
{
|
||||||
public CreateDeveloperReleaseUpdateRequestValidator()
|
public CreateDeveloperReleaseUpdateRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Title).NotEmpty().MaximumLength(160);
|
RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(160);
|
||||||
RuleFor(x => x.Summary).NotEmpty().MaximumLength(512);
|
RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(4000);
|
||||||
RuleFor(x => x.Body).MaximumLength(8000);
|
RuleFor(x => x.TitleFr).NotEmpty().MaximumLength(160);
|
||||||
RuleFor(x => x.Category).NotEmpty().MaximumLength(32);
|
RuleFor(x => x.DescriptionFr).NotEmpty().MaximumLength(4000);
|
||||||
RuleFor(x => x.Importance).NotEmpty().MaximumLength(32);
|
|
||||||
RuleFor(x => x.Audience).NotEmpty().MaximumLength(32);
|
|
||||||
RuleFor(x => x.DeploymentLabel).MaximumLength(128);
|
|
||||||
RuleFor(x => x.BuildVersion).MaximumLength(128);
|
|
||||||
RuleFor(x => x.CommitRange).MaximumLength(256);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,26 +37,15 @@ internal class CreateDeveloperReleaseUpdateHandler(AppDbContext dbContext)
|
|||||||
|
|
||||||
public override async Task HandleAsync(CreateDeveloperReleaseUpdateRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateDeveloperReleaseUpdateRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!TryParseRequest(request, out ReleaseUpdateCategory category, out ReleaseUpdateImportance importance, out ReleaseUpdateAudience audience))
|
|
||||||
{
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
ReleaseUpdate update = new()
|
ReleaseUpdate update = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Title = request.Title.Trim(),
|
Title = request.TitleEn.Trim(),
|
||||||
Summary = request.Summary.Trim(),
|
Summary = request.DescriptionEn.Trim(),
|
||||||
Body = NormalizeOptional(request.Body),
|
TitleFr = request.TitleFr.Trim(),
|
||||||
Category = category,
|
SummaryFr = request.DescriptionFr.Trim(),
|
||||||
Importance = importance,
|
|
||||||
Audience = audience,
|
|
||||||
Status = ReleaseUpdateStatus.Draft,
|
Status = ReleaseUpdateStatus.Draft,
|
||||||
DeploymentLabel = NormalizeOptional(request.DeploymentLabel),
|
|
||||||
BuildVersion = NormalizeOptional(request.BuildVersion),
|
|
||||||
CommitRange = NormalizeOptional(request.CommitRange),
|
|
||||||
CreatedByUserId = User.GetUserId(),
|
CreatedByUserId = User.GetUserId(),
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
UpdatedAt = now,
|
UpdatedAt = now,
|
||||||
@@ -78,38 +56,4 @@ internal class CreateDeveloperReleaseUpdateHandler(AppDbContext dbContext)
|
|||||||
|
|
||||||
await SendAsync(update.ToDto(false), StatusCodes.Status201Created, ct);
|
await SendAsync(update.ToDto(false), StatusCodes.Status201Created, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryParseRequest(
|
|
||||||
CreateDeveloperReleaseUpdateRequest request,
|
|
||||||
out ReleaseUpdateCategory category,
|
|
||||||
out ReleaseUpdateImportance importance,
|
|
||||||
out ReleaseUpdateAudience audience)
|
|
||||||
{
|
|
||||||
bool isValid = true;
|
|
||||||
if (!ReleaseUpdateRules.TryParseCategory(request.Category, out category))
|
|
||||||
{
|
|
||||||
AddError(x => x.Category, "The selected release update category is not valid.");
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReleaseUpdateRules.TryParseImportance(request.Importance, out importance))
|
|
||||||
{
|
|
||||||
AddError(x => x.Importance, "The selected release update importance is not valid.");
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReleaseUpdateRules.TryParseAudience(request.Audience, out audience))
|
|
||||||
{
|
|
||||||
AddError(x => x.Audience, "The selected release update audience is not valid.");
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? NormalizeOptional(string? value)
|
|
||||||
{
|
|
||||||
string? normalized = value?.Trim();
|
|
||||||
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ internal class GetUnreadReleaseUpdatesHandler(AppDbContext dbContext)
|
|||||||
public override async Task HandleAsync(CancellationToken ct)
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
Guid userId = User.GetUserId();
|
Guid userId = User.GetUserId();
|
||||||
ReleaseUpdateAudienceContext audienceContext =
|
|
||||||
await ReleaseUpdateVisibility.GetAudienceContextAsync(dbContext, User, userId, ct);
|
|
||||||
|
|
||||||
List<ReleaseUpdate> unreadUpdates = await dbContext.ReleaseUpdates
|
List<ReleaseUpdate> unreadUpdates = await dbContext.ReleaseUpdates
|
||||||
.VisibleTo(audienceContext)
|
.VisibleToUsers()
|
||||||
.Where(update => !dbContext.ReleaseUpdateReadReceipts.Any(receipt =>
|
.Where(update => !dbContext.ReleaseUpdateReadReceipts.Any(receipt =>
|
||||||
receipt.ReleaseUpdateId == update.Id &&
|
receipt.ReleaseUpdateId == update.Id &&
|
||||||
receipt.UserId == userId))
|
receipt.UserId == userId))
|
||||||
@@ -35,7 +33,7 @@ internal class GetUnreadReleaseUpdatesHandler(AppDbContext dbContext)
|
|||||||
await SendOkAsync(
|
await SendOkAsync(
|
||||||
new ReleaseUpdateUnreadSummaryDto(
|
new ReleaseUpdateUnreadSummaryDto(
|
||||||
unreadUpdates.Count,
|
unreadUpdates.Count,
|
||||||
unreadUpdates.Count(update => update.Importance == ReleaseUpdateImportance.Important),
|
0,
|
||||||
unreadUpdates.Select(update => update.ToDto(false)).ToArray()),
|
unreadUpdates.Select(update => update.ToDto(false)).ToArray()),
|
||||||
ct);
|
ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
using FastEndpoints;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Socialize.Api.Data;
|
|
||||||
using Socialize.Api.Modules.Identity.Contracts;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Services;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
|
||||||
|
|
||||||
internal record ImportDeveloperReleaseCommitDto(
|
|
||||||
string Sha,
|
|
||||||
string? ShortSha,
|
|
||||||
string Subject,
|
|
||||||
string? AuthorName,
|
|
||||||
string? AuthorEmail,
|
|
||||||
DateTimeOffset? AuthoredAt,
|
|
||||||
DateTimeOffset? CommittedAt,
|
|
||||||
string? SourceBranch,
|
|
||||||
string? DeploymentLabel,
|
|
||||||
string? ExternalUrl);
|
|
||||||
|
|
||||||
internal record ImportDeveloperReleaseCommitsRequest(
|
|
||||||
string? SinceSha,
|
|
||||||
string? UntilSha,
|
|
||||||
string? SourceBranch,
|
|
||||||
string? DeploymentLabel,
|
|
||||||
DateTimeOffset? Since,
|
|
||||||
DateTimeOffset? Until,
|
|
||||||
int? Limit,
|
|
||||||
IReadOnlyCollection<ImportDeveloperReleaseCommitDto>? Commits);
|
|
||||||
|
|
||||||
internal class ImportDeveloperReleaseCommitsHandler(
|
|
||||||
AppDbContext dbContext,
|
|
||||||
ReleaseCommitRepositoryImportService repositoryImportService)
|
|
||||||
: Endpoint<ImportDeveloperReleaseCommitsRequest, ReleaseCommitImportResultDto>
|
|
||||||
{
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Post("/api/developer/release-commits/import");
|
|
||||||
Roles(KnownRoles.Developer);
|
|
||||||
Options(o => o.WithTags("Release Communications"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(ImportDeveloperReleaseCommitsRequest request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
IReadOnlyCollection<ReleaseCommit> requestedCommits;
|
|
||||||
if (request.Commits is { Count: > 0 })
|
|
||||||
{
|
|
||||||
requestedCommits = request.Commits.Select(ToReleaseCommit).ToArray();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ReleaseCommitRepositoryImportResult importResult = await repositoryImportService.FetchCommitsAsync(request, ct);
|
|
||||||
if (!importResult.IsSuccess)
|
|
||||||
{
|
|
||||||
AddError(importResult.ErrorMessage ?? "Repository commit import failed.");
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestedCommits = importResult.Commits;
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
AddError(ex.Message);
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
AddError(ex.Message);
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int imported = 0;
|
|
||||||
int updated = 0;
|
|
||||||
int skipped = 0;
|
|
||||||
List<ReleaseCommit> savedCommits = [];
|
|
||||||
foreach (ReleaseCommit requestedCommit in requestedCommits)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(requestedCommit.Sha) || string.IsNullOrWhiteSpace(requestedCommit.Subject))
|
|
||||||
{
|
|
||||||
skipped++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseCommit? existingCommit = await dbContext.ReleaseCommits.SingleOrDefaultAsync(
|
|
||||||
commit => commit.Sha == requestedCommit.Sha,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
if (existingCommit is null)
|
|
||||||
{
|
|
||||||
dbContext.ReleaseCommits.Add(requestedCommit);
|
|
||||||
savedCommits.Add(requestedCommit);
|
|
||||||
imported++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
existingCommit.ShortSha = requestedCommit.ShortSha;
|
|
||||||
existingCommit.Subject = requestedCommit.Subject;
|
|
||||||
existingCommit.AuthorName = requestedCommit.AuthorName;
|
|
||||||
existingCommit.AuthorEmail = requestedCommit.AuthorEmail;
|
|
||||||
existingCommit.AuthoredAt = requestedCommit.AuthoredAt;
|
|
||||||
existingCommit.CommittedAt = requestedCommit.CommittedAt;
|
|
||||||
existingCommit.SourceBranch = requestedCommit.SourceBranch ?? existingCommit.SourceBranch;
|
|
||||||
existingCommit.DeploymentLabel = requestedCommit.DeploymentLabel ?? existingCommit.DeploymentLabel;
|
|
||||||
existingCommit.ExternalUrl = requestedCommit.ExternalUrl ?? existingCommit.ExternalUrl;
|
|
||||||
existingCommit.UpdatedAt = DateTimeOffset.UtcNow;
|
|
||||||
savedCommits.Add(existingCommit);
|
|
||||||
updated++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
|
||||||
await SendOkAsync(
|
|
||||||
new ReleaseCommitImportResultDto(imported, updated, skipped, savedCommits.Select(commit => commit.ToDto()).ToArray()),
|
|
||||||
ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ReleaseCommit ToReleaseCommit(ImportDeveloperReleaseCommitDto dto)
|
|
||||||
{
|
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
|
||||||
return new ReleaseCommit
|
|
||||||
{
|
|
||||||
Sha = dto.Sha.Trim(),
|
|
||||||
ShortSha = NormalizeOptional(dto.ShortSha) ?? dto.Sha.Trim()[..Math.Min(dto.Sha.Trim().Length, 12)],
|
|
||||||
Subject = dto.Subject.Trim(),
|
|
||||||
AuthorName = NormalizeOptional(dto.AuthorName),
|
|
||||||
AuthorEmail = NormalizeOptional(dto.AuthorEmail),
|
|
||||||
AuthoredAt = ToUtc(dto.AuthoredAt),
|
|
||||||
CommittedAt = ToUtc(dto.CommittedAt),
|
|
||||||
SourceBranch = NormalizeOptional(dto.SourceBranch),
|
|
||||||
DeploymentLabel = NormalizeOptional(dto.DeploymentLabel),
|
|
||||||
ExternalUrl = NormalizeOptional(dto.ExternalUrl),
|
|
||||||
CommunicationStatus = ReleaseCommitCommunicationStatus.Unreviewed,
|
|
||||||
ImportedAt = now,
|
|
||||||
UpdatedAt = now,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? NormalizeOptional(string? value)
|
|
||||||
{
|
|
||||||
string? normalized = value?.Trim();
|
|
||||||
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DateTimeOffset? ToUtc(DateTimeOffset? value)
|
|
||||||
{
|
|
||||||
return value?.ToUniversalTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,11 +20,9 @@ internal class ListReleaseUpdatesHandler(AppDbContext dbContext)
|
|||||||
public override async Task HandleAsync(CancellationToken ct)
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
Guid userId = User.GetUserId();
|
Guid userId = User.GetUserId();
|
||||||
ReleaseUpdateAudienceContext audienceContext =
|
|
||||||
await ReleaseUpdateVisibility.GetAudienceContextAsync(dbContext, User, userId, ct);
|
|
||||||
|
|
||||||
List<ReleaseUpdate> updates = await dbContext.ReleaseUpdates
|
List<ReleaseUpdate> updates = await dbContext.ReleaseUpdates
|
||||||
.VisibleTo(audienceContext)
|
.VisibleToUsers()
|
||||||
.OrderByDescending(update => update.PublishedAt)
|
.OrderByDescending(update => update.PublishedAt)
|
||||||
.ThenByDescending(update => update.CreatedAt)
|
.ThenByDescending(update => update.CreatedAt)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|||||||
@@ -19,11 +19,9 @@ internal class MarkAllReleaseUpdatesReadHandler(AppDbContext dbContext)
|
|||||||
public override async Task HandleAsync(CancellationToken ct)
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
Guid userId = User.GetUserId();
|
Guid userId = User.GetUserId();
|
||||||
ReleaseUpdateAudienceContext audienceContext =
|
|
||||||
await ReleaseUpdateVisibility.GetAudienceContextAsync(dbContext, User, userId, ct);
|
|
||||||
|
|
||||||
List<Guid> visibleUpdateIds = await dbContext.ReleaseUpdates
|
List<Guid> visibleUpdateIds = await dbContext.ReleaseUpdates
|
||||||
.VisibleTo(audienceContext)
|
.VisibleToUsers()
|
||||||
.Select(update => update.Id)
|
.Select(update => update.Id)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,9 @@ internal class MarkReleaseUpdateReadHandler(AppDbContext dbContext)
|
|||||||
{
|
{
|
||||||
Guid id = Route<Guid>("id");
|
Guid id = Route<Guid>("id");
|
||||||
Guid userId = User.GetUserId();
|
Guid userId = User.GetUserId();
|
||||||
ReleaseUpdateAudienceContext audienceContext =
|
|
||||||
await ReleaseUpdateVisibility.GetAudienceContextAsync(dbContext, User, userId, ct);
|
|
||||||
|
|
||||||
bool canReadUpdate = await dbContext.ReleaseUpdates
|
bool canReadUpdate = await dbContext.ReleaseUpdates
|
||||||
.VisibleTo(audienceContext)
|
.VisibleToUsers()
|
||||||
.AnyAsync(update => update.Id == id, ct);
|
.AnyAsync(update => update.Id == id, ct);
|
||||||
|
|
||||||
if (!canReadUpdate)
|
if (!canReadUpdate)
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Services;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
||||||
|
|
||||||
|
internal class RefreshDeveloperReleaseCommitsHandler(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
ReleaseCommitRepositoryRefreshService repositoryRefreshService)
|
||||||
|
: EndpointWithoutRequest<ReleaseCommitRefreshResultDto>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/developer/release-commits/refresh");
|
||||||
|
Roles(KnownRoles.Developer);
|
||||||
|
Options(o => o.WithTags("Release Communications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<ReleaseCommit> requestedCommits;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ReleaseCommitRepositoryRefreshResult refreshResult = await repositoryRefreshService.FetchCommitsAsync(ct);
|
||||||
|
if (!refreshResult.IsSuccess)
|
||||||
|
{
|
||||||
|
AddError(refreshResult.ErrorMessage ?? "Repository commit refresh failed.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedCommits = refreshResult.Commits;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
AddError(ex.Message);
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
AddError(ex.Message);
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int created = 0;
|
||||||
|
int updated = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
List<ReleaseCommit> savedCommits = [];
|
||||||
|
foreach (ReleaseCommit requestedCommit in requestedCommits)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(requestedCommit.Sha) || string.IsNullOrWhiteSpace(requestedCommit.Subject))
|
||||||
|
{
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseCommit? existingCommit = await dbContext.ReleaseCommits.SingleOrDefaultAsync(
|
||||||
|
commit => commit.Sha == requestedCommit.Sha,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (existingCommit is null)
|
||||||
|
{
|
||||||
|
dbContext.ReleaseCommits.Add(requestedCommit);
|
||||||
|
savedCommits.Add(requestedCommit);
|
||||||
|
created++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingCommit.ShortSha = requestedCommit.ShortSha;
|
||||||
|
existingCommit.Subject = requestedCommit.Subject;
|
||||||
|
existingCommit.AuthorName = requestedCommit.AuthorName;
|
||||||
|
existingCommit.AuthorEmail = requestedCommit.AuthorEmail;
|
||||||
|
existingCommit.AuthoredAt = requestedCommit.AuthoredAt;
|
||||||
|
existingCommit.CommittedAt = requestedCommit.CommittedAt;
|
||||||
|
existingCommit.SourceBranch = requestedCommit.SourceBranch ?? existingCommit.SourceBranch;
|
||||||
|
existingCommit.DeploymentLabel = requestedCommit.DeploymentLabel ?? existingCommit.DeploymentLabel;
|
||||||
|
existingCommit.ExternalUrl = requestedCommit.ExternalUrl ?? existingCommit.ExternalUrl;
|
||||||
|
existingCommit.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
savedCommits.Add(existingCommit);
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
await SendOkAsync(
|
||||||
|
new ReleaseCommitRefreshResultDto(created, updated, skipped, savedCommits.Select(commit => commit.ToDto()).ToArray()),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using FastEndpoints;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Socialize.Api.Data;
|
|
||||||
using Socialize.Api.Infrastructure.Security;
|
|
||||||
using Socialize.Api.Modules.Identity.Contracts;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Services;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
|
||||||
|
|
||||||
internal record SendDeveloperReleaseUpdateEmailRequest(
|
|
||||||
bool TestMode,
|
|
||||||
bool ConfirmResend);
|
|
||||||
|
|
||||||
internal class SendDeveloperReleaseUpdateEmailHandler(
|
|
||||||
AppDbContext dbContext,
|
|
||||||
ReleaseUpdateEmailService emailService)
|
|
||||||
: Endpoint<SendDeveloperReleaseUpdateEmailRequest, ReleaseUpdateEmailSendResultDto>
|
|
||||||
{
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Post("/api/developer/release-updates/{id}/send-email");
|
|
||||||
Roles(KnownRoles.Developer);
|
|
||||||
Options(o => o.WithTags("Release Communications"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(SendDeveloperReleaseUpdateEmailRequest request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
Guid id = Route<Guid>("id");
|
|
||||||
ReleaseUpdate? update = await dbContext.ReleaseUpdates.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
|
|
||||||
if (update is null)
|
|
||||||
{
|
|
||||||
await SendNotFoundAsync(ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ReleaseUpdateEmailSendResultDto result = await emailService.SendManualUpdateEmailAsync(
|
|
||||||
update,
|
|
||||||
User.GetUserId(),
|
|
||||||
request.TestMode,
|
|
||||||
request.ConfirmResend,
|
|
||||||
ct);
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
|
||||||
await SendOkAsync(result, ct);
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
AddError(ex.Message);
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,8 @@ namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
|||||||
|
|
||||||
internal record LinkDeveloperReleaseCommitRequest(Guid ReleaseUpdateId);
|
internal record LinkDeveloperReleaseCommitRequest(Guid ReleaseUpdateId);
|
||||||
|
|
||||||
|
internal record LinkFirstReleaseCommitsRequest(Guid ReleaseUpdateId);
|
||||||
|
|
||||||
internal abstract class ReleaseCommitStatusEndpoint(AppDbContext dbContext)
|
internal abstract class ReleaseCommitStatusEndpoint(AppDbContext dbContext)
|
||||||
: EndpointWithoutRequest<ReleaseCommitDto>
|
: EndpointWithoutRequest<ReleaseCommitDto>
|
||||||
{
|
{
|
||||||
@@ -67,6 +69,70 @@ internal class LinkDeveloperReleaseCommitHandler(AppDbContext dbContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class LinkFirstReleaseCommitsHandler(AppDbContext dbContext)
|
||||||
|
: Endpoint<LinkFirstReleaseCommitsRequest, ReleaseCommitBulkLinkResultDto>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/developer/release-commits/{sha}/link-first-release");
|
||||||
|
Roles(KnownRoles.Developer);
|
||||||
|
Options(o => o.WithTags("Release Communications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(LinkFirstReleaseCommitsRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
string? sha = Route<string>("sha");
|
||||||
|
if (string.IsNullOrWhiteSpace(sha))
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool releaseUpdateExists = await dbContext.ReleaseUpdates
|
||||||
|
.AnyAsync(update => update.Id == request.ReleaseUpdateId, ct);
|
||||||
|
ReleaseCommit? anchorCommit = await dbContext.ReleaseCommits
|
||||||
|
.SingleOrDefaultAsync(commit => commit.Sha == sha, ct);
|
||||||
|
|
||||||
|
if (!releaseUpdateExists || anchorCommit is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchorCommit.ReleaseUpdateId is not null ||
|
||||||
|
anchorCommit.CommunicationStatus != ReleaseCommitCommunicationStatus.Unreviewed)
|
||||||
|
{
|
||||||
|
AddError("The selected first release commit must be unlinked and unreviewed.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset anchorDate = CommitDate(anchorCommit);
|
||||||
|
List<ReleaseCommit> commits = await dbContext.ReleaseCommits
|
||||||
|
.Where(commit =>
|
||||||
|
commit.ReleaseUpdateId == null &&
|
||||||
|
commit.CommunicationStatus == ReleaseCommitCommunicationStatus.Unreviewed &&
|
||||||
|
(commit.CommittedAt ?? commit.AuthoredAt ?? commit.ImportedAt) <= anchorDate)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
foreach (ReleaseCommit commit in commits)
|
||||||
|
{
|
||||||
|
commit.ReleaseUpdateId = request.ReleaseUpdateId;
|
||||||
|
commit.CommunicationStatus = ReleaseCommitCommunicationStatus.Linked;
|
||||||
|
commit.UpdatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
await SendOkAsync(new ReleaseCommitBulkLinkResultDto(commits.Count), ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset CommitDate(ReleaseCommit commit)
|
||||||
|
{
|
||||||
|
return commit.CommittedAt ?? commit.AuthoredAt ?? commit.ImportedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class UnlinkDeveloperReleaseCommitHandler(AppDbContext dbContext)
|
internal class UnlinkDeveloperReleaseCommitHandler(AppDbContext dbContext)
|
||||||
: ReleaseCommitStatusEndpoint(dbContext)
|
: ReleaseCommitStatusEndpoint(dbContext)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,35 +4,24 @@ using Socialize.Api.Data;
|
|||||||
using Socialize.Api.Modules.Identity.Contracts;
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Services;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
||||||
|
|
||||||
internal record UpdateDeveloperReleaseUpdateRequest(
|
internal record UpdateDeveloperReleaseUpdateRequest(
|
||||||
string Title,
|
string TitleEn,
|
||||||
string Summary,
|
string DescriptionEn,
|
||||||
string? Body,
|
string TitleFr,
|
||||||
string Category,
|
string DescriptionFr);
|
||||||
string Importance,
|
|
||||||
string Audience,
|
|
||||||
string? DeploymentLabel,
|
|
||||||
string? BuildVersion,
|
|
||||||
string? CommitRange);
|
|
||||||
|
|
||||||
internal class UpdateDeveloperReleaseUpdateRequestValidator
|
internal class UpdateDeveloperReleaseUpdateRequestValidator
|
||||||
: Validator<UpdateDeveloperReleaseUpdateRequest>
|
: Validator<UpdateDeveloperReleaseUpdateRequest>
|
||||||
{
|
{
|
||||||
public UpdateDeveloperReleaseUpdateRequestValidator()
|
public UpdateDeveloperReleaseUpdateRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Title).NotEmpty().MaximumLength(160);
|
RuleFor(x => x.TitleEn).NotEmpty().MaximumLength(160);
|
||||||
RuleFor(x => x.Summary).NotEmpty().MaximumLength(512);
|
RuleFor(x => x.DescriptionEn).NotEmpty().MaximumLength(4000);
|
||||||
RuleFor(x => x.Body).MaximumLength(8000);
|
RuleFor(x => x.TitleFr).NotEmpty().MaximumLength(160);
|
||||||
RuleFor(x => x.Category).NotEmpty().MaximumLength(32);
|
RuleFor(x => x.DescriptionFr).NotEmpty().MaximumLength(4000);
|
||||||
RuleFor(x => x.Importance).NotEmpty().MaximumLength(32);
|
|
||||||
RuleFor(x => x.Audience).NotEmpty().MaximumLength(32);
|
|
||||||
RuleFor(x => x.DeploymentLabel).MaximumLength(128);
|
|
||||||
RuleFor(x => x.BuildVersion).MaximumLength(128);
|
|
||||||
RuleFor(x => x.CommitRange).MaximumLength(256);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,58 +52,13 @@ internal class UpdateDeveloperReleaseUpdateHandler(AppDbContext dbContext)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryParseRequest(request, out ReleaseUpdateCategory category, out ReleaseUpdateImportance importance, out ReleaseUpdateAudience audience))
|
update.Title = request.TitleEn.Trim();
|
||||||
{
|
update.Summary = request.DescriptionEn.Trim();
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
update.TitleFr = request.TitleFr.Trim();
|
||||||
return;
|
update.SummaryFr = request.DescriptionFr.Trim();
|
||||||
}
|
|
||||||
|
|
||||||
update.Title = request.Title.Trim();
|
|
||||||
update.Summary = request.Summary.Trim();
|
|
||||||
update.Body = NormalizeOptional(request.Body);
|
|
||||||
update.Category = category;
|
|
||||||
update.Importance = importance;
|
|
||||||
update.Audience = audience;
|
|
||||||
update.DeploymentLabel = NormalizeOptional(request.DeploymentLabel);
|
|
||||||
update.BuildVersion = NormalizeOptional(request.BuildVersion);
|
|
||||||
update.CommitRange = NormalizeOptional(request.CommitRange);
|
|
||||||
update.UpdatedAt = DateTimeOffset.UtcNow;
|
update.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
await SendOkAsync(update.ToDto(false), ct);
|
await SendOkAsync(update.ToDto(false), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryParseRequest(
|
|
||||||
UpdateDeveloperReleaseUpdateRequest request,
|
|
||||||
out ReleaseUpdateCategory category,
|
|
||||||
out ReleaseUpdateImportance importance,
|
|
||||||
out ReleaseUpdateAudience audience)
|
|
||||||
{
|
|
||||||
bool isValid = true;
|
|
||||||
if (!ReleaseUpdateRules.TryParseCategory(request.Category, out category))
|
|
||||||
{
|
|
||||||
AddError(x => x.Category, "The selected release update category is not valid.");
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReleaseUpdateRules.TryParseImportance(request.Importance, out importance))
|
|
||||||
{
|
|
||||||
AddError(x => x.Importance, "The selected release update importance is not valid.");
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ReleaseUpdateRules.TryParseAudience(request.Audience, out audience))
|
|
||||||
{
|
|
||||||
AddError(x => x.Audience, "The selected release update audience is not valid.");
|
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? NormalizeOptional(string? value)
|
|
||||||
{
|
|
||||||
string? normalized = value?.Trim();
|
|
||||||
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ internal static class ModuleRegistration
|
|||||||
builder.Services.Configure<ReleaseCommunicationRepositoryOptions>(
|
builder.Services.Configure<ReleaseCommunicationRepositoryOptions>(
|
||||||
builder.Configuration.GetSection(ReleaseCommunicationRepositoryOptions.SectionName));
|
builder.Configuration.GetSection(ReleaseCommunicationRepositoryOptions.SectionName));
|
||||||
builder.Services.AddScoped<ReleaseUpdateEmailService>();
|
builder.Services.AddScoped<ReleaseUpdateEmailService>();
|
||||||
builder.Services.AddScoped<ReleaseCommitRepositoryImportService>();
|
builder.Services.AddScoped<ReleaseCommitRepositoryRefreshService>();
|
||||||
builder.Services.AddHostedService<ReleaseUpdateEmailDigestBackgroundService>();
|
builder.Services.AddHostedService<ReleaseUpdateEmailDigestBackgroundService>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
|
|||||||
@@ -5,46 +5,41 @@ using System.Text.Json;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Configuration;
|
using Socialize.Api.Modules.ReleaseCommunications.Configuration;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
||||||
|
|
||||||
internal sealed record ReleaseCommitRepositoryImportResult(
|
internal sealed record ReleaseCommitRepositoryRefreshResult(
|
||||||
IReadOnlyCollection<ReleaseCommit> Commits,
|
IReadOnlyCollection<ReleaseCommit> Commits,
|
||||||
string? ErrorMessage)
|
string? ErrorMessage)
|
||||||
{
|
{
|
||||||
public bool IsSuccess => ErrorMessage is null;
|
public bool IsSuccess => ErrorMessage is null;
|
||||||
|
|
||||||
public static ReleaseCommitRepositoryImportResult Success(IReadOnlyCollection<ReleaseCommit> commits)
|
public static ReleaseCommitRepositoryRefreshResult Success(IReadOnlyCollection<ReleaseCommit> commits)
|
||||||
{
|
{
|
||||||
return new ReleaseCommitRepositoryImportResult(commits, null);
|
return new ReleaseCommitRepositoryRefreshResult(commits, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReleaseCommitRepositoryImportResult Failure(string errorMessage)
|
public static ReleaseCommitRepositoryRefreshResult Failure(string errorMessage)
|
||||||
{
|
{
|
||||||
return new ReleaseCommitRepositoryImportResult([], errorMessage);
|
return new ReleaseCommitRepositoryRefreshResult([], errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class ReleaseCommitRepositoryImportService(
|
internal sealed class ReleaseCommitRepositoryRefreshService(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptionsSnapshot<ReleaseCommunicationRepositoryOptions> repositoryOptions)
|
IOptionsSnapshot<ReleaseCommunicationRepositoryOptions> repositoryOptions)
|
||||||
{
|
{
|
||||||
private const int DefaultLimit = 50;
|
private const int DefaultLimit = 50;
|
||||||
private const int MaxLimit = 100;
|
|
||||||
|
|
||||||
public async Task<ReleaseCommitRepositoryImportResult> FetchCommitsAsync(
|
public async Task<ReleaseCommitRepositoryRefreshResult> FetchCommitsAsync(
|
||||||
ImportDeveloperReleaseCommitsRequest request,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
ReleaseCommunicationRepositoryOptions options = repositoryOptions.Value;
|
ReleaseCommunicationRepositoryOptions options = repositoryOptions.Value;
|
||||||
if (!TryBuildApiTarget(options.RepositoryUrl, out RepositoryApiTarget target, out string? targetError))
|
if (!TryBuildApiTarget(options.RepositoryUrl, out RepositoryApiTarget target, out string? targetError))
|
||||||
{
|
{
|
||||||
return ReleaseCommitRepositoryImportResult.Failure(targetError ?? "Repository configuration is not valid.");
|
return ReleaseCommitRepositoryRefreshResult.Failure(targetError ?? "Repository configuration is not valid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
int limit = Math.Clamp(request.Limit ?? DefaultLimit, 1, MaxLimit);
|
|
||||||
|
|
||||||
using HttpClient httpClient = httpClientFactory.CreateClient();
|
using HttpClient httpClient = httpClientFactory.CreateClient();
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Socialize", "1.0"));
|
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Socialize", "1.0"));
|
||||||
if (!string.IsNullOrWhiteSpace(options.AccessToken))
|
if (!string.IsNullOrWhiteSpace(options.AccessToken))
|
||||||
@@ -52,11 +47,11 @@ internal sealed class ReleaseCommitRepositoryImportService(
|
|||||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", options.AccessToken.Trim());
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", options.AccessToken.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
using HttpResponseMessage response = await httpClient.GetAsync(BuildRequestUri(target, request, limit), ct);
|
using HttpResponseMessage response = await httpClient.GetAsync(BuildRequestUri(target), ct);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return ReleaseCommitRepositoryImportResult.Failure(
|
return ReleaseCommitRepositoryRefreshResult.Failure(
|
||||||
$"Repository commit import failed with HTTP {(int)response.StatusCode} ({response.ReasonPhrase}).");
|
$"Repository commit refresh failed with HTTP {(int)response.StatusCode} ({response.ReasonPhrase}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
await using Stream stream = await response.Content.ReadAsStreamAsync(ct);
|
await using Stream stream = await response.Content.ReadAsStreamAsync(ct);
|
||||||
@@ -73,56 +68,31 @@ internal sealed class ReleaseCommitRepositoryImportService(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return ReleaseCommitRepositoryImportResult.Failure("Repository API response did not include a commit list.");
|
return ReleaseCommitRepositoryRefreshResult.Failure("Repository API response did not include a commit list.");
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
List<ReleaseCommit> commits = [];
|
List<ReleaseCommit> commits = [];
|
||||||
foreach (JsonElement commitElement in commitsElement.EnumerateArray())
|
foreach (JsonElement commitElement in commitsElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
ReleaseCommit? commit = ToReleaseCommit(commitElement, request, now);
|
ReleaseCommit? commit = ToReleaseCommit(commitElement, now);
|
||||||
if (commit is not null)
|
if (commit is not null)
|
||||||
{
|
{
|
||||||
commits.Add(commit);
|
commits.Add(commit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ReleaseCommitRepositoryImportResult.Success(commits);
|
return ReleaseCommitRepositoryRefreshResult.Success(commits);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri BuildRequestUri(
|
private static Uri BuildRequestUri(RepositoryApiTarget target)
|
||||||
RepositoryApiTarget target,
|
|
||||||
ImportDeveloperReleaseCommitsRequest request,
|
|
||||||
int limit)
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(request.SinceSha) && !string.IsNullOrWhiteSpace(request.UntilSha))
|
|
||||||
{
|
|
||||||
string baseHead = $"{request.SinceSha.Trim()}...{request.UntilSha.Trim()}";
|
|
||||||
return new Uri($"{target.ApiBaseUri}/compare/{Uri.EscapeDataString(baseHead)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<string, string> query = new(StringComparer.Ordinal)
|
Dictionary<string, string> query = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
["limit"] = limit.ToString(CultureInfo.InvariantCulture),
|
["limit"] = DefaultLimit.ToString(CultureInfo.InvariantCulture),
|
||||||
["page"] = "1",
|
["page"] = "1",
|
||||||
};
|
};
|
||||||
|
|
||||||
string? sha = NormalizeOptional(request.UntilSha) ?? NormalizeOptional(request.SourceBranch);
|
|
||||||
if (sha is not null)
|
|
||||||
{
|
|
||||||
query["sha"] = sha;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Since.HasValue)
|
|
||||||
{
|
|
||||||
query["since"] = request.Since.Value.UtcDateTime.ToString("O", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Until.HasValue)
|
|
||||||
{
|
|
||||||
query["until"] = request.Until.Value.UtcDateTime.ToString("O", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
string queryString = string.Join(
|
string queryString = string.Join(
|
||||||
"&",
|
"&",
|
||||||
query.Select(pair => $"{WebUtility.UrlEncode(pair.Key)}={WebUtility.UrlEncode(pair.Value)}"));
|
query.Select(pair => $"{WebUtility.UrlEncode(pair.Key)}={WebUtility.UrlEncode(pair.Value)}"));
|
||||||
@@ -140,7 +110,7 @@ internal sealed class ReleaseCommitRepositoryImportService(
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(repositoryUrl))
|
if (string.IsNullOrWhiteSpace(repositoryUrl))
|
||||||
{
|
{
|
||||||
errorMessage = "ReleaseCommunications:Repository:RepositoryUrl is required before repository import can be used.";
|
errorMessage = "ReleaseCommunications:Repository:RepositoryUrl is required before repository refresh can be used.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +146,6 @@ internal sealed class ReleaseCommitRepositoryImportService(
|
|||||||
|
|
||||||
private static ReleaseCommit? ToReleaseCommit(
|
private static ReleaseCommit? ToReleaseCommit(
|
||||||
JsonElement commitElement,
|
JsonElement commitElement,
|
||||||
ImportDeveloperReleaseCommitsRequest request,
|
|
||||||
DateTimeOffset now)
|
DateTimeOffset now)
|
||||||
{
|
{
|
||||||
string? sha = GetString(commitElement, "sha") ?? GetString(commitElement, "id");
|
string? sha = GetString(commitElement, "sha") ?? GetString(commitElement, "id");
|
||||||
@@ -211,8 +180,8 @@ internal sealed class ReleaseCommitRepositoryImportService(
|
|||||||
AuthorEmail = authorElement.HasValue ? NormalizeOptional(GetString(authorElement.Value, "email")) : null,
|
AuthorEmail = authorElement.HasValue ? NormalizeOptional(GetString(authorElement.Value, "email")) : null,
|
||||||
AuthoredAt = authorElement.HasValue ? GetUtcDateTimeOffset(authorElement.Value, "date") : null,
|
AuthoredAt = authorElement.HasValue ? GetUtcDateTimeOffset(authorElement.Value, "date") : null,
|
||||||
CommittedAt = committerElement.HasValue ? GetUtcDateTimeOffset(committerElement.Value, "date") : null,
|
CommittedAt = committerElement.HasValue ? GetUtcDateTimeOffset(committerElement.Value, "date") : null,
|
||||||
SourceBranch = NormalizeOptional(request.SourceBranch),
|
SourceBranch = null,
|
||||||
DeploymentLabel = NormalizeOptional(request.DeploymentLabel),
|
DeploymentLabel = null,
|
||||||
ExternalUrl = NormalizeOptional(GetString(commitElement, "html_url") ?? GetString(commitElement, "url")),
|
ExternalUrl = NormalizeOptional(GetString(commitElement, "html_url") ?? GetString(commitElement, "url")),
|
||||||
CommunicationStatus = ReleaseCommitCommunicationStatus.Unreviewed,
|
CommunicationStatus = ReleaseCommitCommunicationStatus.Unreviewed,
|
||||||
ImportedAt = now,
|
ImportedAt = now,
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Configuration;
|
using Socialize.Api.Infrastructure.Configuration;
|
||||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
using Socialize.Api.Modules.Identity.Contracts;
|
|
||||||
using Socialize.Api.Modules.Identity.Data;
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
using Socialize.Api.Modules.Organizations.Services;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
||||||
@@ -20,48 +16,6 @@ internal class ReleaseUpdateEmailService(
|
|||||||
IEmailSender emailSender,
|
IEmailSender emailSender,
|
||||||
IOptionsSnapshot<WebsiteOptions> websiteOptions)
|
IOptionsSnapshot<WebsiteOptions> websiteOptions)
|
||||||
{
|
{
|
||||||
public async Task<ReleaseUpdateEmailSendResultDto> SendManualUpdateEmailAsync(
|
|
||||||
ReleaseUpdate update,
|
|
||||||
Guid senderUserId,
|
|
||||||
bool testMode,
|
|
||||||
bool confirmResend,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (update.Status != ReleaseUpdateStatus.Published)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Only published release updates can be emailed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!testMode && update.ManualEmailSentAt.HasValue && !confirmResend)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("This release update was already emailed. Confirm resend to send it again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
IReadOnlyCollection<User> recipients = testMode
|
|
||||||
? await GetTestRecipientsAsync(senderUserId, ct)
|
|
||||||
: await GetAudienceRecipientsAsync(update.Audience, ct);
|
|
||||||
|
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
|
||||||
foreach (User recipient in recipients.Where(recipient => !string.IsNullOrWhiteSpace(recipient.Email)))
|
|
||||||
{
|
|
||||||
await emailSender.SendEmailAsync(
|
|
||||||
recipient.Email!,
|
|
||||||
$"What's new in Socialize: {update.Title}",
|
|
||||||
BuildSingleUpdateEmail(update));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!testMode)
|
|
||||||
{
|
|
||||||
update.ManualEmailSentByUserId = senderUserId;
|
|
||||||
update.ManualEmailSentAt = now;
|
|
||||||
update.ManualEmailAudience = update.Audience.ToString();
|
|
||||||
update.ManualEmailRecipientCount = recipients.Count;
|
|
||||||
update.UpdatedAt = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ReleaseUpdateEmailSendResultDto(recipients.Count, now, testMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> SendDueDigestEmailsAsync(
|
public async Task<int> SendDueDigestEmailsAsync(
|
||||||
TimeSpan inactiveThreshold,
|
TimeSpan inactiveThreshold,
|
||||||
TimeSpan sendInterval,
|
TimeSpan sendInterval,
|
||||||
@@ -71,7 +25,7 @@ internal class ReleaseUpdateEmailService(
|
|||||||
DateTimeOffset inactiveBefore = now.Subtract(inactiveThreshold);
|
DateTimeOffset inactiveBefore = now.Subtract(inactiveThreshold);
|
||||||
DateTimeOffset lastSentBefore = now.Subtract(sendInterval);
|
DateTimeOffset lastSentBefore = now.Subtract(sendInterval);
|
||||||
|
|
||||||
List<User> ownerUsers = await GetAudienceRecipientsAsync(ReleaseUpdateAudience.OrganizationOwners, ct);
|
List<User> ownerUsers = await GetReleaseNoteRecipientsAsync(ct);
|
||||||
int sentCount = 0;
|
int sentCount = 0;
|
||||||
foreach (User user in ownerUsers)
|
foreach (User user in ownerUsers)
|
||||||
{
|
{
|
||||||
@@ -91,14 +45,8 @@ internal class ReleaseUpdateEmailService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReleaseUpdateAudienceContext audienceContext = await ReleaseUpdateVisibility.GetAudienceContextAsync(
|
|
||||||
dbContext,
|
|
||||||
new ClaimsPrincipal(new ClaimsIdentity()),
|
|
||||||
user.Id,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
List<ReleaseUpdate> unreadUpdates = await dbContext.ReleaseUpdates
|
List<ReleaseUpdate> unreadUpdates = await dbContext.ReleaseUpdates
|
||||||
.VisibleTo(audienceContext)
|
.VisibleToUsers()
|
||||||
.Where(update => !dbContext.ReleaseUpdateReadReceipts.Any(receipt =>
|
.Where(update => !dbContext.ReleaseUpdateReadReceipts.Any(receipt =>
|
||||||
receipt.ReleaseUpdateId == update.Id &&
|
receipt.ReleaseUpdateId == update.Id &&
|
||||||
receipt.UserId == user.Id))
|
receipt.UserId == user.Id))
|
||||||
@@ -130,48 +78,12 @@ internal class ReleaseUpdateEmailService(
|
|||||||
return sentCount;
|
return sentCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyCollection<User>> GetTestRecipientsAsync(Guid senderUserId, CancellationToken ct)
|
private async Task<List<User>> GetReleaseNoteRecipientsAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
User? sender = await userManager.Users.SingleOrDefaultAsync(user => user.Id == senderUserId, ct);
|
return await userManager.Users
|
||||||
return sender is null ? [] : [sender];
|
.Where(user => user.EmailConfirmed && user.Email != null)
|
||||||
}
|
.OrderBy(user => user.Email)
|
||||||
|
.ToListAsync(ct);
|
||||||
private async Task<List<User>> GetAudienceRecipientsAsync(ReleaseUpdateAudience audience, CancellationToken ct)
|
|
||||||
{
|
|
||||||
IQueryable<User> query = userManager.Users.Where(user => user.EmailConfirmed && user.Email != null);
|
|
||||||
|
|
||||||
if (audience == ReleaseUpdateAudience.Developers)
|
|
||||||
{
|
|
||||||
IList<User> developers = await userManager.GetUsersInRoleAsync(KnownRoles.Developer);
|
|
||||||
return developers.Where(user => user.EmailConfirmed && !string.IsNullOrWhiteSpace(user.Email)).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audience == ReleaseUpdateAudience.OrganizationOwners)
|
|
||||||
{
|
|
||||||
Guid[] ownerUserIds = await dbContext.Organizations
|
|
||||||
.Select(organization => organization.OwnerUserId)
|
|
||||||
.Concat(dbContext.OrganizationMemberships
|
|
||||||
.Where(membership => membership.Role == OrganizationRoles.Owner)
|
|
||||||
.Select(membership => membership.UserId))
|
|
||||||
.Distinct()
|
|
||||||
.ToArrayAsync(ct);
|
|
||||||
|
|
||||||
query = query.Where(user => ownerUserIds.Contains(user.Id));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.OrderBy(user => user.Email).ToListAsync(ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildSingleUpdateEmail(ReleaseUpdate update)
|
|
||||||
{
|
|
||||||
string updateUrl = $"{websiteOptions.Value.FrontendBaseUrl.TrimEnd('/')}/app/updates?updateId={update.Id}";
|
|
||||||
return $"""
|
|
||||||
<h1>{HtmlEncode(update.Title)}</h1>
|
|
||||||
<p><strong>{HtmlEncode(update.Category.ToString())}</strong></p>
|
|
||||||
<p>{HtmlEncode(update.Summary)}</p>
|
|
||||||
{FormatBody(update.Body)}
|
|
||||||
<p><a href="{HtmlEncode(updateUrl)}">Open What's New</a></p>
|
|
||||||
""";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildDigestEmail(IReadOnlyCollection<ReleaseUpdate> updates)
|
private string BuildDigestEmail(IReadOnlyCollection<ReleaseUpdate> updates)
|
||||||
@@ -179,7 +91,12 @@ internal class ReleaseUpdateEmailService(
|
|||||||
string updateUrl = $"{websiteOptions.Value.FrontendBaseUrl.TrimEnd('/')}/app/updates";
|
string updateUrl = $"{websiteOptions.Value.FrontendBaseUrl.TrimEnd('/')}/app/updates";
|
||||||
string listItems = string.Join(
|
string listItems = string.Join(
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
updates.Select(update => $"<li><strong>{HtmlEncode(update.Title)}</strong><br>{HtmlEncode(update.Summary)}</li>"));
|
updates.Select(update => $"""
|
||||||
|
<li>
|
||||||
|
<strong>{HtmlEncode(update.Title)}</strong><br>{HtmlEncode(update.Summary)}<br>
|
||||||
|
<strong>{HtmlEncode(update.TitleFr)}</strong><br>{HtmlEncode(update.SummaryFr)}
|
||||||
|
</li>
|
||||||
|
"""));
|
||||||
|
|
||||||
return $"""
|
return $"""
|
||||||
<h1>What's new in Socialize</h1>
|
<h1>What's new in Socialize</h1>
|
||||||
@@ -188,13 +105,6 @@ internal class ReleaseUpdateEmailService(
|
|||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatBody(string? body)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(body)
|
|
||||||
? string.Empty
|
|
||||||
: $"<p>{HtmlEncode(body).Replace(Environment.NewLine, "<br>", StringComparison.Ordinal)}</p>";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string HtmlEncode(string? value)
|
private static string HtmlEncode(string? value)
|
||||||
{
|
{
|
||||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
|
||||||
|
|
||||||
internal static class ReleaseUpdateRules
|
|
||||||
{
|
|
||||||
public static bool TryParseCategory(string value, out ReleaseUpdateCategory category)
|
|
||||||
{
|
|
||||||
return TryParseEnum(value, out category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryParseImportance(string value, out ReleaseUpdateImportance importance)
|
|
||||||
{
|
|
||||||
return TryParseEnum(value, out importance);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryParseAudience(string value, out ReleaseUpdateAudience audience)
|
|
||||||
{
|
|
||||||
return TryParseEnum(value, out audience);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseEnum<TEnum>(string value, out TEnum result)
|
|
||||||
where TEnum : struct
|
|
||||||
{
|
|
||||||
string normalized = value.Replace(" ", string.Empty, StringComparison.Ordinal);
|
|
||||||
return Enum.TryParse(normalized, ignoreCase: true, out result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +1,11 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Socialize.Api.Data;
|
|
||||||
using Socialize.Api.Modules.Identity.Contracts;
|
|
||||||
using Socialize.Api.Modules.Organizations.Data;
|
|
||||||
using Socialize.Api.Modules.Organizations.Services;
|
|
||||||
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
namespace Socialize.Api.Modules.ReleaseCommunications.Services;
|
||||||
|
|
||||||
internal static class ReleaseUpdateVisibility
|
internal static class ReleaseUpdateVisibility
|
||||||
{
|
{
|
||||||
public static async Task<ReleaseUpdateAudienceContext> GetAudienceContextAsync(
|
public static IQueryable<ReleaseUpdate> VisibleToUsers(this IQueryable<ReleaseUpdate> query)
|
||||||
AppDbContext dbContext,
|
|
||||||
ClaimsPrincipal user,
|
|
||||||
Guid userId,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
bool isDeveloper = user.IsInRole(KnownRoles.Developer);
|
return query.Where(update => update.Status == ReleaseUpdateStatus.Published);
|
||||||
bool isOrganizationOwner = await dbContext.Organizations.AnyAsync(
|
|
||||||
organization => organization.OwnerUserId == userId,
|
|
||||||
ct)
|
|
||||||
|| await dbContext.OrganizationMemberships.AnyAsync(
|
|
||||||
membership =>
|
|
||||||
membership.UserId == userId &&
|
|
||||||
membership.Role == OrganizationRoles.Owner,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
return new ReleaseUpdateAudienceContext(isDeveloper, isOrganizationOwner);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<ReleaseUpdate> VisibleTo(
|
|
||||||
this IQueryable<ReleaseUpdate> query,
|
|
||||||
ReleaseUpdateAudienceContext context)
|
|
||||||
{
|
|
||||||
return query.Where(update =>
|
|
||||||
update.Status == ReleaseUpdateStatus.Published &&
|
|
||||||
(update.Audience == ReleaseUpdateAudience.Everyone ||
|
|
||||||
(update.Audience == ReleaseUpdateAudience.OrganizationOwners && context.IsOrganizationOwner) ||
|
|
||||||
(update.Audience == ReleaseUpdateAudience.Developers && context.IsDeveloper)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal record ReleaseUpdateAudienceContext(
|
|
||||||
bool IsDeveloper,
|
|
||||||
bool IsOrganizationOwner);
|
|
||||||
|
|||||||
@@ -6,64 +6,16 @@ namespace Socialize.Tests.ReleaseCommunications;
|
|||||||
|
|
||||||
public class ReleaseUpdateRulesTests
|
public class ReleaseUpdateRulesTests
|
||||||
{
|
{
|
||||||
[Theory]
|
|
||||||
[InlineData("Feature", ReleaseUpdateCategory.Feature)]
|
|
||||||
[InlineData("improvement", ReleaseUpdateCategory.Improvement)]
|
|
||||||
[InlineData("Breaking Change", ReleaseUpdateCategory.BreakingChange)]
|
|
||||||
[InlineData("BreakingChange", ReleaseUpdateCategory.BreakingChange)]
|
|
||||||
internal void TryParseCategory_accepts_supported_categories(string value, ReleaseUpdateCategory expected)
|
|
||||||
{
|
|
||||||
bool parsed = ReleaseUpdateRules.TryParseCategory(value, out ReleaseUpdateCategory category);
|
|
||||||
|
|
||||||
Assert.True(parsed);
|
|
||||||
Assert.Equal(expected, category);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("")]
|
|
||||||
[InlineData("Security")]
|
|
||||||
[InlineData("Maintenance")]
|
|
||||||
public void TryParseCategory_rejects_unsupported_categories(string value)
|
|
||||||
{
|
|
||||||
bool parsed = ReleaseUpdateRules.TryParseCategory(value, out _);
|
|
||||||
|
|
||||||
Assert.False(parsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("Normal", ReleaseUpdateImportance.Normal)]
|
|
||||||
[InlineData("important", ReleaseUpdateImportance.Important)]
|
|
||||||
internal void TryParseImportance_accepts_supported_importance(string value, ReleaseUpdateImportance expected)
|
|
||||||
{
|
|
||||||
bool parsed = ReleaseUpdateRules.TryParseImportance(value, out ReleaseUpdateImportance importance);
|
|
||||||
|
|
||||||
Assert.True(parsed);
|
|
||||||
Assert.Equal(expected, importance);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData("Everyone", ReleaseUpdateAudience.Everyone)]
|
|
||||||
[InlineData("Organization Owners", ReleaseUpdateAudience.OrganizationOwners)]
|
|
||||||
[InlineData("developers", ReleaseUpdateAudience.Developers)]
|
|
||||||
internal void TryParseAudience_accepts_supported_audiences(string value, ReleaseUpdateAudience expected)
|
|
||||||
{
|
|
||||||
bool parsed = ReleaseUpdateRules.TryParseAudience(value, out ReleaseUpdateAudience audience);
|
|
||||||
|
|
||||||
Assert.True(parsed);
|
|
||||||
Assert.Equal(expected, audience);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToDto_formats_breaking_change_category_for_display()
|
public void ToDto_maps_summary_to_description()
|
||||||
{
|
{
|
||||||
ReleaseUpdate update = new()
|
ReleaseUpdate update = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Title = "API change",
|
Title = "API change",
|
||||||
Summary = "A workflow API changed.",
|
Summary = "A workflow API changed.",
|
||||||
Category = ReleaseUpdateCategory.BreakingChange,
|
TitleFr = "Changement API",
|
||||||
Importance = ReleaseUpdateImportance.Important,
|
SummaryFr = "Une API du flux de travail a change.",
|
||||||
Audience = ReleaseUpdateAudience.Developers,
|
|
||||||
Status = ReleaseUpdateStatus.Published,
|
Status = ReleaseUpdateStatus.Published,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
UpdatedAt = DateTimeOffset.UtcNow,
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
@@ -73,18 +25,22 @@ public class ReleaseUpdateRulesTests
|
|||||||
|
|
||||||
ReleaseUpdateDto dto = update.ToDto(isRead: true);
|
ReleaseUpdateDto dto = update.ToDto(isRead: true);
|
||||||
|
|
||||||
Assert.Equal("Breaking Change", dto.Category);
|
Assert.Equal("A workflow API changed.", dto.Description);
|
||||||
|
Assert.Equal("API change", dto.TitleEn);
|
||||||
|
Assert.Equal("A workflow API changed.", dto.DescriptionEn);
|
||||||
|
Assert.Equal("Changement API", dto.TitleFr);
|
||||||
|
Assert.Equal("Une API du flux de travail a change.", dto.DescriptionFr);
|
||||||
Assert.True(dto.IsRead);
|
Assert.True(dto.IsRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VisibleTo_returns_everyone_updates_for_any_authenticated_user()
|
public void VisibleToUsers_returns_published_updates()
|
||||||
{
|
{
|
||||||
ReleaseUpdate update = NewPublishedUpdate(ReleaseUpdateAudience.Everyone);
|
ReleaseUpdate update = NewPublishedUpdate();
|
||||||
|
|
||||||
List<ReleaseUpdate> visibleUpdates = new[] { update }
|
List<ReleaseUpdate> visibleUpdates = new[] { update }
|
||||||
.AsQueryable()
|
.AsQueryable()
|
||||||
.VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: false, IsOrganizationOwner: false))
|
.VisibleToUsers()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
Assert.Same(update, Assert.Single(visibleUpdates));
|
Assert.Same(update, Assert.Single(visibleUpdates));
|
||||||
@@ -93,37 +49,17 @@ public class ReleaseUpdateRulesTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void VisibleTo_rejects_unpublished_updates()
|
public void VisibleTo_rejects_unpublished_updates()
|
||||||
{
|
{
|
||||||
ReleaseUpdate update = NewPublishedUpdate(ReleaseUpdateAudience.Everyone);
|
ReleaseUpdate update = NewPublishedUpdate();
|
||||||
update.Status = ReleaseUpdateStatus.Draft;
|
update.Status = ReleaseUpdateStatus.Draft;
|
||||||
|
|
||||||
List<ReleaseUpdate> visibleUpdates = new[] { update }
|
List<ReleaseUpdate> visibleUpdates = new[] { update }
|
||||||
.AsQueryable()
|
.AsQueryable()
|
||||||
.VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: true, IsOrganizationOwner: true))
|
.VisibleToUsers()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
Assert.Empty(visibleUpdates);
|
Assert.Empty(visibleUpdates);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void VisibleTo_requires_matching_restricted_audience()
|
|
||||||
{
|
|
||||||
ReleaseUpdate ownerUpdate = NewPublishedUpdate(ReleaseUpdateAudience.OrganizationOwners);
|
|
||||||
ReleaseUpdate developerUpdate = NewPublishedUpdate(ReleaseUpdateAudience.Developers);
|
|
||||||
|
|
||||||
List<ReleaseUpdate> ownerVisibleUpdates = new[] { ownerUpdate, developerUpdate }
|
|
||||||
.AsQueryable()
|
|
||||||
.VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: false, IsOrganizationOwner: true))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
List<ReleaseUpdate> developerVisibleUpdates = new[] { ownerUpdate, developerUpdate }
|
|
||||||
.AsQueryable()
|
|
||||||
.VisibleTo(new ReleaseUpdateAudienceContext(IsDeveloper: true, IsOrganizationOwner: false))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
Assert.Same(ownerUpdate, Assert.Single(ownerVisibleUpdates));
|
|
||||||
Assert.Same(developerUpdate, Assert.Single(developerVisibleUpdates));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateMissingReadReceipts_creates_receipts_only_for_unread_visible_updates()
|
public void CreateMissingReadReceipts_creates_receipts_only_for_unread_visible_updates()
|
||||||
{
|
{
|
||||||
@@ -172,16 +108,15 @@ public class ReleaseUpdateRulesTests
|
|||||||
Assert.False(ReleaseUpdateEmailRules.CanSendDigest(lastSentBefore.AddMinutes(1), lastSentBefore));
|
Assert.False(ReleaseUpdateEmailRules.CanSendDigest(lastSentBefore.AddMinutes(1), lastSentBefore));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReleaseUpdate NewPublishedUpdate(ReleaseUpdateAudience audience)
|
private static ReleaseUpdate NewPublishedUpdate()
|
||||||
{
|
{
|
||||||
return new ReleaseUpdate
|
return new ReleaseUpdate
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Title = "Update",
|
Title = "Update",
|
||||||
Summary = "Something changed.",
|
Summary = "Something changed.",
|
||||||
Category = ReleaseUpdateCategory.Improvement,
|
TitleFr = "Mise a jour",
|
||||||
Importance = ReleaseUpdateImportance.Normal,
|
SummaryFr = "Quelque chose a change.",
|
||||||
Audience = audience,
|
|
||||||
Status = ReleaseUpdateStatus.Published,
|
Status = ReleaseUpdateStatus.Published,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
UpdatedAt = DateTimeOffset.UtcNow,
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ The system should not rely only on the user's last login timestamp. Login is one
|
|||||||
- Developer-only release communication back office:
|
- Developer-only release communication back office:
|
||||||
- `/app/developer/updates`
|
- `/app/developer/updates`
|
||||||
- `/app/developer/updates/:id`
|
- `/app/developer/updates/:id`
|
||||||
- `/app/developer/release-commits`
|
- `/app/developer/release-notes`
|
||||||
|
|
||||||
Feature-owned frontend code belongs under:
|
Feature-owned frontend code belongs under:
|
||||||
|
|
||||||
@@ -235,9 +235,8 @@ GET /api/developer/release-updates/{id}
|
|||||||
PUT /api/developer/release-updates/{id}
|
PUT /api/developer/release-updates/{id}
|
||||||
POST /api/developer/release-updates/{id}/publish
|
POST /api/developer/release-updates/{id}/publish
|
||||||
POST /api/developer/release-updates/{id}/archive
|
POST /api/developer/release-updates/{id}/archive
|
||||||
POST /api/developer/release-updates/{id}/send-email
|
|
||||||
GET /api/developer/release-commits
|
GET /api/developer/release-commits
|
||||||
POST /api/developer/release-commits/import
|
POST /api/developer/release-commits/refresh
|
||||||
POST /api/developer/release-commits/{sha}/link
|
POST /api/developer/release-commits/{sha}/link
|
||||||
POST /api/developer/release-commits/{sha}/unlink
|
POST /api/developer/release-commits/{sha}/unlink
|
||||||
POST /api/developer/release-commits/{sha}/internal-only
|
POST /api/developer/release-commits/{sha}/internal-only
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Add the developer back-office workflow for importing shipped commits and matchin
|
|||||||
- mark a commit internal-only
|
- mark a commit internal-only
|
||||||
- mark a commit ignored
|
- mark a commit ignored
|
||||||
- Add developer-only frontend screens:
|
- Add developer-only frontend screens:
|
||||||
- `/app/developer/release-commits`
|
- `/app/developer/release-notes`
|
||||||
- linked commits on `/app/developer/updates/:id`
|
- linked commits on `/app/developer/updates/:id`
|
||||||
- Add repository-backed import from configured HTTPS repository settings.
|
- Add repository-backed import from configured HTTPS repository settings.
|
||||||
- Add a selected-commit workflow to copy commit SHA/details and create a draft update entry linked to those commits.
|
- Add a selected-commit workflow to copy commit SHA/details and create a draft update entry linked to those commits.
|
||||||
|
|||||||
228
frontend/src/api/schema.d.ts
vendored
228
frontend/src/api/schema.d.ts
vendored
@@ -164,22 +164,6 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/developer/release-commits/import": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsHandler"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/api/developer/release-commits": {
|
"/api/developer/release-commits": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -260,7 +244,7 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/developer/release-updates/{id}/send-email": {
|
"/api/developer/release-commits/refresh": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -269,7 +253,7 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
post: operations["SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailHandler"];
|
post: operations["SocializeApiModulesReleaseCommunicationsHandlersRefreshDeveloperReleaseCommitsHandler"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
head?: never;
|
head?: never;
|
||||||
@@ -292,6 +276,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/developer/release-commits/{sha}/link-first-release": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsHandler"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/developer/release-commits/{sha}/unlink": {
|
"/api/developer/release-commits/{sha}/unlink": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1471,15 +1471,12 @@ export interface components {
|
|||||||
/** Format: guid */
|
/** Format: guid */
|
||||||
id?: string;
|
id?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
summary?: string;
|
description?: string;
|
||||||
body?: string | null;
|
titleEn?: string;
|
||||||
category?: string;
|
descriptionEn?: string;
|
||||||
importance?: string;
|
titleFr?: string;
|
||||||
audience?: string;
|
descriptionFr?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
deploymentLabel?: string | null;
|
|
||||||
buildVersion?: string | null;
|
|
||||||
commitRange?: string | null;
|
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
@@ -1488,25 +1485,13 @@ export interface components {
|
|||||||
publishedAt?: string | null;
|
publishedAt?: string | null;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
archivedAt?: string | null;
|
archivedAt?: string | null;
|
||||||
/** Format: guid */
|
|
||||||
manualEmailSentByUserId?: string | null;
|
|
||||||
/** Format: date-time */
|
|
||||||
manualEmailSentAt?: string | null;
|
|
||||||
manualEmailAudience?: string | null;
|
|
||||||
/** Format: int32 */
|
|
||||||
manualEmailRecipientCount?: number | null;
|
|
||||||
isRead?: boolean;
|
isRead?: boolean;
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersCreateDeveloperReleaseUpdateRequest: {
|
SocializeApiModulesReleaseCommunicationsHandlersCreateDeveloperReleaseUpdateRequest: {
|
||||||
title: string;
|
titleEn: string;
|
||||||
summary: string;
|
descriptionEn: string;
|
||||||
body?: string | null;
|
titleFr: string;
|
||||||
category: string;
|
descriptionFr: string;
|
||||||
importance: string;
|
|
||||||
audience: string;
|
|
||||||
deploymentLabel?: string | null;
|
|
||||||
buildVersion?: string | null;
|
|
||||||
commitRange?: string | null;
|
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto: {
|
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto: {
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
@@ -1515,15 +1500,6 @@ export interface components {
|
|||||||
importantUnreadCount?: number;
|
importantUnreadCount?: number;
|
||||||
updates?: components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDto"][];
|
updates?: components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDto"][];
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsContractsReleaseCommitImportResultDto: {
|
|
||||||
/** Format: int32 */
|
|
||||||
importedCount?: number;
|
|
||||||
/** Format: int32 */
|
|
||||||
updatedCount?: number;
|
|
||||||
/** Format: int32 */
|
|
||||||
skippedCount?: number;
|
|
||||||
commits?: components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto"][];
|
|
||||||
};
|
|
||||||
SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto: {
|
SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto: {
|
||||||
sha?: string;
|
sha?: string;
|
||||||
shortSha?: string;
|
shortSha?: string;
|
||||||
@@ -1545,52 +1521,32 @@ export interface components {
|
|||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
updatedAt?: string;
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsRequest: {
|
SocializeApiModulesReleaseCommunicationsContractsReleaseCommitRefreshResultDto: {
|
||||||
sinceSha?: string | null;
|
|
||||||
untilSha?: string | null;
|
|
||||||
sourceBranch?: string | null;
|
|
||||||
deploymentLabel?: string | null;
|
|
||||||
commits?: components["schemas"]["SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitDto"][] | null;
|
|
||||||
};
|
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitDto: {
|
|
||||||
sha?: string;
|
|
||||||
shortSha?: string | null;
|
|
||||||
subject?: string;
|
|
||||||
authorName?: string | null;
|
|
||||||
authorEmail?: string | null;
|
|
||||||
/** Format: date-time */
|
|
||||||
authoredAt?: string | null;
|
|
||||||
/** Format: date-time */
|
|
||||||
committedAt?: string | null;
|
|
||||||
sourceBranch?: string | null;
|
|
||||||
deploymentLabel?: string | null;
|
|
||||||
externalUrl?: string | null;
|
|
||||||
};
|
|
||||||
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateEmailSendResultDto: {
|
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
recipientCount?: number;
|
createdCount?: number;
|
||||||
/** Format: date-time */
|
/** Format: int32 */
|
||||||
sentAt?: string;
|
updatedCount?: number;
|
||||||
testMode?: boolean;
|
/** Format: int32 */
|
||||||
};
|
skippedCount?: number;
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailRequest: {
|
commits?: components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto"][];
|
||||||
testMode?: boolean;
|
|
||||||
confirmResend?: boolean;
|
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersLinkDeveloperReleaseCommitRequest: {
|
SocializeApiModulesReleaseCommunicationsHandlersLinkDeveloperReleaseCommitRequest: {
|
||||||
/** Format: guid */
|
/** Format: guid */
|
||||||
releaseUpdateId?: string;
|
releaseUpdateId?: string;
|
||||||
};
|
};
|
||||||
|
SocializeApiModulesReleaseCommunicationsContractsReleaseCommitBulkLinkResultDto: {
|
||||||
|
/** Format: int32 */
|
||||||
|
linkedCount?: number;
|
||||||
|
};
|
||||||
|
SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsRequest: {
|
||||||
|
/** Format: guid */
|
||||||
|
releaseUpdateId?: string;
|
||||||
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersUpdateDeveloperReleaseUpdateRequest: {
|
SocializeApiModulesReleaseCommunicationsHandlersUpdateDeveloperReleaseUpdateRequest: {
|
||||||
title: string;
|
titleEn: string;
|
||||||
summary: string;
|
descriptionEn: string;
|
||||||
body?: string | null;
|
titleFr: string;
|
||||||
category: string;
|
descriptionFr: string;
|
||||||
importance: string;
|
|
||||||
audience: string;
|
|
||||||
deploymentLabel?: string | null;
|
|
||||||
buildVersion?: string | null;
|
|
||||||
commitRange?: string | null;
|
|
||||||
};
|
};
|
||||||
SocializeApiModulesOrganizationsHandlersOrganizationMemberDto: {
|
SocializeApiModulesOrganizationsHandlersOrganizationMemberDto: {
|
||||||
/** Format: guid */
|
/** Format: guid */
|
||||||
@@ -2871,44 +2827,6 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsHandler: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsRequest"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
/** @description Success */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseCommitImportResultDto"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/** @description Unauthorized */
|
|
||||||
401: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Forbidden */
|
|
||||||
403: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersListDeveloperReleaseCommitsHandler: {
|
SocializeApiModulesReleaseCommunicationsHandlersListDeveloperReleaseCommitsHandler: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -3058,20 +2976,14 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailHandler: {
|
SocializeApiModulesReleaseCommunicationsHandlersRefreshDeveloperReleaseCommitsHandler: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
path: {
|
path?: never;
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
requestBody: {
|
requestBody?: never;
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailRequest"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
responses: {
|
||||||
/** @description Success */
|
/** @description Success */
|
||||||
200: {
|
200: {
|
||||||
@@ -3079,7 +2991,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateEmailSendResultDto"];
|
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseCommitRefreshResultDto"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Unauthorized */
|
/** @description Unauthorized */
|
||||||
@@ -3138,6 +3050,46 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsHandler: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
sha: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Success */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseCommitBulkLinkResultDto"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Forbidden */
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersUnlinkDeveloperReleaseCommitHandler: {
|
SocializeApiModulesReleaseCommunicationsHandlersUnlinkDeveloperReleaseCommitHandler: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss/theme.css";
|
||||||
|
@import "tailwindcss/utilities.css";
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -29,59 +30,6 @@ textarea::placeholder {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-application {
|
|
||||||
background: rgb(var(--v-theme-background)) !important;
|
|
||||||
color: rgb(var(--v-theme-on-background));
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-card,
|
|
||||||
.v-sheet,
|
|
||||||
.v-list,
|
|
||||||
.v-menu > .v-overlay__content,
|
|
||||||
.v-dialog > .v-overlay__content {
|
|
||||||
background-color: rgb(var(--v-theme-surface)) !important;
|
|
||||||
border: 1px solid rgb(var(--v-theme-border));
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-field {
|
|
||||||
background-color: rgb(var(--v-theme-control)) !important;
|
|
||||||
color: rgb(var(--v-theme-on-surface));
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-field:hover {
|
|
||||||
background-color: rgb(var(--v-theme-control-hover)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-field--focused {
|
|
||||||
background-color: rgb(var(--v-theme-control-focus)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-field__outline {
|
|
||||||
color: rgb(var(--v-theme-border-strong));
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-field--focused .v-field__outline {
|
|
||||||
color: rgb(var(--v-theme-highlight));
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-field__input,
|
|
||||||
.v-field-label {
|
|
||||||
color: rgb(var(--v-theme-on-surface));
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-select .v-field .v-field__input > input,
|
|
||||||
.v-select .v-field .v-field__input > input::placeholder {
|
|
||||||
color: transparent !important;
|
|
||||||
caret-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel,
|
|
||||||
[class$='-panel'],
|
|
||||||
[class$='-card'],
|
|
||||||
div.card {
|
|
||||||
border-color: rgb(var(--v-theme-border)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.btn {
|
.btn {
|
||||||
@apply min-w-24 w-full;
|
@apply min-w-24 w-full;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
export function formatReleaseDescription(value) {
|
||||||
|
const lines = String(value ?? '').replace(/\r\n?/g, '\n').split('\n');
|
||||||
|
const blocks = [];
|
||||||
|
let paragraphLines = [];
|
||||||
|
let listItems = [];
|
||||||
|
|
||||||
|
function flushParagraph() {
|
||||||
|
if (!paragraphLines.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
type: 'paragraph',
|
||||||
|
text: paragraphLines.join(' ').trim(),
|
||||||
|
});
|
||||||
|
paragraphLines = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushList() {
|
||||||
|
if (!listItems.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.push({
|
||||||
|
type: 'list',
|
||||||
|
items: listItems,
|
||||||
|
});
|
||||||
|
listItems = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.forEach(line => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
flushParagraph();
|
||||||
|
flushList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bullet = trimmed.match(/^[-*]\s+(.+)$/);
|
||||||
|
if (bullet) {
|
||||||
|
flushParagraph();
|
||||||
|
listItems.push(bullet[1].trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushList();
|
||||||
|
paragraphLines.push(trimmed);
|
||||||
|
});
|
||||||
|
|
||||||
|
flushParagraph();
|
||||||
|
flushList();
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
@@ -3,17 +3,9 @@ import { defineStore } from 'pinia';
|
|||||||
import { useClient } from '@/plugins/api.js';
|
import { useClient } from '@/plugins/api.js';
|
||||||
|
|
||||||
const DEFAULT_COMMIT_FILTERS = Object.freeze({
|
const DEFAULT_COMMIT_FILTERS = Object.freeze({
|
||||||
status: '',
|
inclusion: 'notIncluded',
|
||||||
updateId: '',
|
|
||||||
author: '',
|
|
||||||
search: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RELEASE_UPDATE_CATEGORIES = ['Feature', 'Improvement', 'Fix', 'Breaking Change'];
|
|
||||||
export const RELEASE_UPDATE_IMPORTANCE = ['Normal', 'Important'];
|
|
||||||
export const RELEASE_UPDATE_AUDIENCES = ['Everyone', 'OrganizationOwners', 'Developers'];
|
|
||||||
export const RELEASE_COMMIT_STATUSES = ['Unreviewed', 'Linked', 'InternalOnly', 'Ignored'];
|
|
||||||
|
|
||||||
export const useReleaseCommunicationsStore = defineStore('release-communications', () => {
|
export const useReleaseCommunicationsStore = defineStore('release-communications', () => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const updates = ref([]);
|
const updates = ref([]);
|
||||||
@@ -21,11 +13,11 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
const developerUpdates = ref([]);
|
const developerUpdates = ref([]);
|
||||||
const selectedUpdate = ref(null);
|
const selectedUpdate = ref(null);
|
||||||
const commits = ref([]);
|
const commits = ref([]);
|
||||||
|
const selectedCommitShas = ref([]);
|
||||||
const commitFilters = ref({ ...DEFAULT_COMMIT_FILTERS });
|
const commitFilters = ref({ ...DEFAULT_COMMIT_FILTERS });
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const isSendingEmail = ref(false);
|
const isRefreshingCommits = ref(false);
|
||||||
const isImporting = ref(false);
|
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
|
|
||||||
const unreadCount = computed(() => unreadSummary.value?.unreadCount ?? 0);
|
const unreadCount = computed(() => unreadSummary.value?.unreadCount ?? 0);
|
||||||
@@ -35,40 +27,15 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filteredCommits = computed(() => {
|
const filteredCommits = computed(() => {
|
||||||
const query = commitFilters.value.search.trim().toLowerCase();
|
|
||||||
const author = commitFilters.value.author.trim().toLowerCase();
|
|
||||||
|
|
||||||
return commits.value.filter(commit => {
|
return commits.value.filter(commit => {
|
||||||
if (commitFilters.value.status && commit.communicationStatus !== commitFilters.value.status) {
|
if (commitFilters.value.inclusion === 'included' && !commit.releaseUpdateId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commitFilters.value.updateId && commit.releaseUpdateId !== commitFilters.value.updateId) {
|
if (commitFilters.value.inclusion === 'notIncluded' && commit.releaseUpdateId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author) {
|
|
||||||
const authorText = `${commit.authorName ?? ''} ${commit.authorEmail ?? ''}`.toLowerCase();
|
|
||||||
if (!authorText.includes(author)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
const haystack = [
|
|
||||||
commit.sha,
|
|
||||||
commit.shortSha,
|
|
||||||
commit.subject,
|
|
||||||
commit.authorName,
|
|
||||||
commit.authorEmail,
|
|
||||||
commit.deploymentLabel,
|
|
||||||
commit.sourceBranch,
|
|
||||||
].filter(Boolean).join(' ').toLowerCase();
|
|
||||||
if (!haystack.includes(query)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -159,28 +126,19 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendUpdateEmail(id, payload) {
|
|
||||||
isSendingEmail.value = true;
|
|
||||||
try {
|
|
||||||
return (await client.post(`/api/developer/release-updates/${id}/send-email`, payload)).data;
|
|
||||||
} finally {
|
|
||||||
isSendingEmail.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCommits() {
|
async function loadCommits() {
|
||||||
const response = await client.get('/api/developer/release-commits');
|
const response = await client.get('/api/developer/release-commits');
|
||||||
commits.value = response.data ?? [];
|
commits.value = response.data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importCommits(payload) {
|
async function refreshCommits() {
|
||||||
isImporting.value = true;
|
isRefreshingCommits.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await client.post('/api/developer/release-commits/import', payload);
|
const response = await client.post('/api/developer/release-commits/refresh');
|
||||||
await loadCommits();
|
await loadCommits();
|
||||||
return response.data;
|
return response.data;
|
||||||
} finally {
|
} finally {
|
||||||
isImporting.value = false;
|
isRefreshingCommits.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +152,12 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
await Promise.all([loadCommits(), loadDeveloperUpdates()]);
|
await Promise.all([loadCommits(), loadDeveloperUpdates()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function linkFirstReleaseCommits(anchorSha, releaseUpdateId) {
|
||||||
|
const response = await client.post(`/api/developer/release-commits/${anchorSha}/link-first-release`, { releaseUpdateId });
|
||||||
|
await Promise.all([loadCommits(), loadDeveloperUpdates()]);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
async function unlinkCommit(sha) {
|
async function unlinkCommit(sha) {
|
||||||
await client.post(`/api/developer/release-commits/${sha}/unlink`);
|
await client.post(`/api/developer/release-commits/${sha}/unlink`);
|
||||||
await loadCommits();
|
await loadCommits();
|
||||||
@@ -213,12 +177,23 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
commitFilters.value = { ...DEFAULT_COMMIT_FILTERS };
|
commitFilters.value = { ...DEFAULT_COMMIT_FILTERS };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setCommitSelected(sha, selected) {
|
||||||
|
selectedCommitShas.value = selected
|
||||||
|
? [...new Set([...selectedCommitShas.value, sha])]
|
||||||
|
: selectedCommitShas.value.filter(selectedSha => selectedSha !== sha);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectedCommits() {
|
||||||
|
selectedCommitShas.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updates,
|
updates,
|
||||||
unreadSummary,
|
unreadSummary,
|
||||||
developerUpdates,
|
developerUpdates,
|
||||||
selectedUpdate,
|
selectedUpdate,
|
||||||
commits,
|
commits,
|
||||||
|
selectedCommitShas,
|
||||||
commitFilters,
|
commitFilters,
|
||||||
filteredCommits,
|
filteredCommits,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
@@ -226,8 +201,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
unreviewedCommitCount,
|
unreviewedCommitCount,
|
||||||
isLoading,
|
isLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
isSendingEmail,
|
isRefreshingCommits,
|
||||||
isImporting,
|
|
||||||
error,
|
error,
|
||||||
loadUserUpdates,
|
loadUserUpdates,
|
||||||
loadUnreadSummary,
|
loadUnreadSummary,
|
||||||
@@ -238,14 +212,16 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
saveDeveloperUpdate,
|
saveDeveloperUpdate,
|
||||||
publishUpdate,
|
publishUpdate,
|
||||||
archiveUpdate,
|
archiveUpdate,
|
||||||
sendUpdateEmail,
|
|
||||||
loadCommits,
|
loadCommits,
|
||||||
importCommits,
|
refreshCommits,
|
||||||
linkCommit,
|
linkCommit,
|
||||||
linkCommitsToUpdate,
|
linkCommitsToUpdate,
|
||||||
|
linkFirstReleaseCommits,
|
||||||
unlinkCommit,
|
unlinkCommit,
|
||||||
markCommitInternalOnly,
|
markCommitInternalOnly,
|
||||||
ignoreCommit,
|
ignoreCommit,
|
||||||
resetCommitFilters,
|
resetCommitFilters,
|
||||||
|
setCommitSelected,
|
||||||
|
clearSelectedCommits,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,246 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import {
|
|
||||||
RELEASE_UPDATE_AUDIENCES,
|
|
||||||
RELEASE_UPDATE_CATEGORIES,
|
|
||||||
RELEASE_UPDATE_IMPORTANCE,
|
|
||||||
useReleaseCommunicationsStore,
|
|
||||||
} from '@/features/release-communications/stores/releaseCommunicationsStore.js';
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const store = useReleaseCommunicationsStore();
|
|
||||||
const editingId = ref(null);
|
|
||||||
const form = reactive({
|
|
||||||
title: '',
|
|
||||||
summary: '',
|
|
||||||
body: '',
|
|
||||||
category: 'Feature',
|
|
||||||
importance: 'Normal',
|
|
||||||
audience: 'Everyone',
|
|
||||||
deploymentLabel: '',
|
|
||||||
buildVersion: '',
|
|
||||||
commitRange: '',
|
|
||||||
});
|
|
||||||
const emailTestMode = ref(true);
|
|
||||||
const confirmResend = ref(false);
|
|
||||||
const emailResult = ref(null);
|
|
||||||
|
|
||||||
const linkedCommits = computed(() =>
|
|
||||||
editingId.value
|
|
||||||
? store.commits.filter(commit => commit.releaseUpdateId === editingId.value)
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await Promise.all([store.loadDeveloperUpdates(), store.loadCommits()]);
|
|
||||||
});
|
|
||||||
|
|
||||||
function editUpdate(update) {
|
|
||||||
editingId.value = update.id;
|
|
||||||
Object.assign(form, {
|
|
||||||
title: update.title ?? '',
|
|
||||||
summary: update.summary ?? '',
|
|
||||||
body: update.body ?? '',
|
|
||||||
category: update.category ?? 'Feature',
|
|
||||||
importance: update.importance ?? 'Normal',
|
|
||||||
audience: update.audience ?? 'Everyone',
|
|
||||||
deploymentLabel: update.deploymentLabel ?? '',
|
|
||||||
buildVersion: update.buildVersion ?? '',
|
|
||||||
commitRange: update.commitRange ?? '',
|
|
||||||
});
|
|
||||||
store.selectedUpdate = update;
|
|
||||||
}
|
|
||||||
|
|
||||||
function newUpdate() {
|
|
||||||
editingId.value = null;
|
|
||||||
Object.assign(form, {
|
|
||||||
title: '',
|
|
||||||
summary: '',
|
|
||||||
body: '',
|
|
||||||
category: 'Feature',
|
|
||||||
importance: 'Normal',
|
|
||||||
audience: 'Everyone',
|
|
||||||
deploymentLabel: '',
|
|
||||||
buildVersion: '',
|
|
||||||
commitRange: '',
|
|
||||||
});
|
|
||||||
emailResult.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
await store.saveDeveloperUpdate({ ...form }, editingId.value);
|
|
||||||
editingId.value = store.selectedUpdate?.id ?? editingId.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendEmail() {
|
|
||||||
if (!editingId.value || !window.confirm(t('releaseCommunications.developer.confirmEmail'))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emailResult.value = await store.sendUpdateEmail(editingId.value, {
|
|
||||||
testMode: emailTestMode.value,
|
|
||||||
confirmResend: confirmResend.value,
|
|
||||||
});
|
|
||||||
await store.loadDeveloperUpdate(editingId.value);
|
|
||||||
await store.loadDeveloperUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(value) {
|
|
||||||
return value ? new Date(value).toLocaleString() : t('releaseCommunications.emptyValue');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="developer-updates-page">
|
|
||||||
<header class="page-header">
|
|
||||||
<div>
|
|
||||||
<div class="eyebrow">{{ t('releaseCommunications.developer.eyebrow') }}</div>
|
|
||||||
<h1>{{ t('releaseCommunications.developer.title') }}</h1>
|
|
||||||
</div>
|
|
||||||
<v-btn @click="newUpdate">{{ t('releaseCommunications.developer.newUpdate') }}</v-btn>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="editor-grid">
|
|
||||||
<form
|
|
||||||
class="editor-panel"
|
|
||||||
@submit.prevent="save"
|
|
||||||
>
|
|
||||||
<v-text-field v-model="form.title" :label="t('title')" density="compact" variant="outlined" />
|
|
||||||
<v-textarea v-model="form.summary" :label="t('releaseCommunications.summary')" rows="2" variant="outlined" />
|
|
||||||
<v-textarea v-model="form.body" :label="t('releaseCommunications.body')" rows="5" variant="outlined" />
|
|
||||||
<div class="form-row">
|
|
||||||
<v-select v-model="form.category" :items="RELEASE_UPDATE_CATEGORIES" :label="t('releaseCommunications.category')" density="compact" variant="outlined" />
|
|
||||||
<v-select v-model="form.importance" :items="RELEASE_UPDATE_IMPORTANCE" :label="t('releaseCommunications.importance')" density="compact" variant="outlined" />
|
|
||||||
<v-select v-model="form.audience" :items="RELEASE_UPDATE_AUDIENCES" :label="t('releaseCommunications.audience')" density="compact" variant="outlined" />
|
|
||||||
</div>
|
|
||||||
<div class="form-row">
|
|
||||||
<v-text-field v-model="form.deploymentLabel" :label="t('releaseCommunications.deploymentLabel')" density="compact" variant="outlined" />
|
|
||||||
<v-text-field v-model="form.buildVersion" :label="t('releaseCommunications.buildVersion')" density="compact" variant="outlined" />
|
|
||||||
<v-text-field v-model="form.commitRange" :label="t('releaseCommunications.commitRange')" density="compact" variant="outlined" />
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<v-btn type="submit" :loading="store.isSaving">{{ t('save') }}</v-btn>
|
|
||||||
<v-btn v-if="editingId" variant="outlined" @click="store.publishUpdate(editingId)">{{ t('releaseCommunications.developer.publish') }}</v-btn>
|
|
||||||
<v-btn v-if="editingId" variant="outlined" @click="store.archiveUpdate(editingId)">{{ t('releaseCommunications.developer.archive') }}</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="editingId"
|
|
||||||
class="email-panel"
|
|
||||||
>
|
|
||||||
<strong>{{ t('releaseCommunications.developer.pushEmail') }}</strong>
|
|
||||||
<v-checkbox v-model="emailTestMode" :label="t('releaseCommunications.developer.testMode')" density="compact" hide-details />
|
|
||||||
<v-checkbox v-model="confirmResend" :label="t('releaseCommunications.developer.confirmResend')" density="compact" hide-details />
|
|
||||||
<v-btn variant="outlined" :loading="store.isSendingEmail" @click="sendEmail">{{ t('releaseCommunications.developer.sendEmail') }}</v-btn>
|
|
||||||
<small v-if="emailResult">{{ t('releaseCommunications.developer.emailResult', { count: emailResult.recipientCount }) }}</small>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<aside class="updates-panel">
|
|
||||||
<button
|
|
||||||
v-for="update in store.developerUpdates"
|
|
||||||
:key="update.id"
|
|
||||||
class="update-row"
|
|
||||||
type="button"
|
|
||||||
@click="editUpdate(update)"
|
|
||||||
>
|
|
||||||
<strong>{{ update.title }}</strong>
|
|
||||||
<span>{{ update.status }} / {{ update.audience }}</span>
|
|
||||||
<small>{{ formatDate(update.publishedAt ?? update.createdAt) }}</small>
|
|
||||||
</button>
|
|
||||||
</aside>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
|
||||||
v-if="editingId"
|
|
||||||
class="linked-commits"
|
|
||||||
>
|
|
||||||
<h2>{{ t('releaseCommunications.developer.linkedCommits') }}</h2>
|
|
||||||
<div v-if="!linkedCommits.length" class="page-message">{{ t('releaseCommunications.developer.noLinkedCommits') }}</div>
|
|
||||||
<div
|
|
||||||
v-for="commit in linkedCommits"
|
|
||||||
:key="commit.sha"
|
|
||||||
class="commit-chip"
|
|
||||||
>
|
|
||||||
<code>{{ commit.shortSha }}</code>
|
|
||||||
<span>{{ commit.subject }}</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.developer-updates-page {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header,
|
|
||||||
.actions,
|
|
||||||
.form-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-header {
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
color: rgb(var(--v-theme-primary));
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.7fr);
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-panel,
|
|
||||||
.updates-panel,
|
|
||||||
.linked-commits {
|
|
||||||
border: 1px solid #d8dee8;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-row {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
gap: 3px;
|
|
||||||
border: 0;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
background: transparent;
|
|
||||||
padding: 10px 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-chip {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.editor-grid,
|
|
||||||
.form-row {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
import { computed, onMounted } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
import { formatReleaseDescription } from '@/features/release-communications/formatReleaseDescription.js';
|
||||||
import { useReleaseCommunicationsStore } from '@/features/release-communications/stores/releaseCommunicationsStore.js';
|
import { useReleaseCommunicationsStore } from '@/features/release-communications/stores/releaseCommunicationsStore.js';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const store = useReleaseCommunicationsStore();
|
const store = useReleaseCommunicationsStore();
|
||||||
|
|
||||||
@@ -12,31 +13,46 @@
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.loadUserUpdates();
|
await store.loadUserUpdates();
|
||||||
if (highlightedId.value) {
|
if (store.updates.some(update => !update.isRead)) {
|
||||||
await store.markRead(highlightedId.value);
|
await store.markAllRead();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
return value ? new Date(value).toLocaleString() : '';
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(locale.value, {
|
||||||
|
dateStyle: 'long',
|
||||||
|
}).format(new Date(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTitle(update) {
|
||||||
|
return locale.value.startsWith('fr')
|
||||||
|
? update.titleFr || update.title
|
||||||
|
: update.titleEn || update.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDescription(update) {
|
||||||
|
return locale.value.startsWith('fr')
|
||||||
|
? update.descriptionFr || update.description
|
||||||
|
: update.descriptionEn || update.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDescriptionBlocks(update) {
|
||||||
|
return formatReleaseDescription(updateDescription(update));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="updates-page">
|
<section class="updates-page">
|
||||||
<header class="updates-header">
|
<header class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<div class="eyebrow">{{ t('releaseCommunications.user.eyebrow') }}</div>
|
<div class="eyebrow">{{ t('releaseCommunications.user.eyebrow') }}</div>
|
||||||
<h1>{{ t('releaseCommunications.user.title') }}</h1>
|
<h1>{{ t('releaseCommunications.user.title') }}</h1>
|
||||||
<p>{{ t('releaseCommunications.user.description') }}</p>
|
<p>{{ t('releaseCommunications.user.description') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<v-btn
|
|
||||||
variant="outlined"
|
|
||||||
:disabled="!store.unreadCount"
|
|
||||||
@click="store.markAllRead"
|
|
||||||
>
|
|
||||||
{{ t('releaseCommunications.user.markAllRead') }}
|
|
||||||
</v-btn>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -55,21 +71,25 @@
|
|||||||
:key="update.id"
|
:key="update.id"
|
||||||
class="update-entry"
|
class="update-entry"
|
||||||
:class="{ 'update-entry-unread': !update.isRead, 'update-entry-highlight': update.id === highlightedId }"
|
:class="{ 'update-entry-unread': !update.isRead, 'update-entry-highlight': update.id === highlightedId }"
|
||||||
@click="!update.isRead && store.markRead(update.id)"
|
|
||||||
>
|
>
|
||||||
<div class="update-meta">
|
<h2>{{ updateTitle(update) }}</h2>
|
||||||
<span>{{ update.category }}</span>
|
<div class="release-description">
|
||||||
<span>{{ update.importance }}</span>
|
<template
|
||||||
|
v-for="(block, index) in updateDescriptionBlocks(update)"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<p v-if="block.type === 'paragraph'">{{ block.text }}</p>
|
||||||
|
<ul v-else>
|
||||||
|
<li
|
||||||
|
v-for="item in block.items"
|
||||||
|
:key="item"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<time>{{ formatDate(update.publishedAt) }}</time>
|
<time>{{ formatDate(update.publishedAt) }}</time>
|
||||||
</div>
|
|
||||||
<h2>{{ update.title }}</h2>
|
|
||||||
<p>{{ update.summary }}</p>
|
|
||||||
<div
|
|
||||||
v-if="update.body"
|
|
||||||
class="update-body"
|
|
||||||
>
|
|
||||||
{{ update.body }}
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -83,81 +103,83 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
.updates-page {
|
.updates-page {
|
||||||
display: grid;
|
@apply mx-auto flex w-full max-w-6xl flex-col gap-5 px-5 py-8 md:px-8;
|
||||||
gap: 20px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.updates-header {
|
.page-header {
|
||||||
display: flex;
|
@apply flex flex-col justify-between gap-4 md:flex-row md:items-start;
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow,
|
.eyebrow {
|
||||||
.update-meta {
|
@apply text-xs font-bold uppercase tracking-[0.22em];
|
||||||
color: rgb(var(--v-theme-primary));
|
color: #0f766e;
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.updates-header h1 {
|
.page-header h1 {
|
||||||
margin: 4px 0;
|
@apply mt-2 text-3xl font-black md:text-4xl;
|
||||||
font-size: 1.75rem;
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
.updates-header p {
|
.page-header p {
|
||||||
margin: 0;
|
@apply mt-2 max-w-3xl text-sm leading-6;
|
||||||
color: #64748b;
|
color: #526178;
|
||||||
}
|
}
|
||||||
|
|
||||||
.updates-list {
|
.updates-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-entry {
|
.update-entry {
|
||||||
border: 1px solid #d8dee8;
|
display: grid;
|
||||||
border-radius: 8px;
|
gap: 8px;
|
||||||
background: #fff;
|
border-bottom: 1px solid #d8dee8;
|
||||||
padding: 16px;
|
padding: 18px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-entry-unread {
|
.update-entry:first-child {
|
||||||
border-color: rgb(var(--v-theme-primary));
|
padding-top: 0;
|
||||||
box-shadow: inset 3px 0 0 rgb(var(--v-theme-primary));
|
}
|
||||||
cursor: pointer;
|
|
||||||
|
.update-entry:last-of-type {
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-entry-highlight {
|
.update-entry-highlight {
|
||||||
outline: 2px solid rgb(var(--v-theme-primary));
|
box-shadow: inset 3px 0 0 rgb(var(--v-theme-primary));
|
||||||
}
|
padding-left: 12px;
|
||||||
|
|
||||||
.update-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-entry h2 {
|
.update-entry h2 {
|
||||||
margin: 0 0 6px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-entry p {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #334155;
|
font-size: 1.1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #172033;
|
||||||
}
|
}
|
||||||
|
|
||||||
.update-body {
|
.release-description {
|
||||||
margin-top: 12px;
|
display: grid;
|
||||||
color: #475569;
|
gap: 8px;
|
||||||
white-space: pre-line;
|
color: #334155;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-description p,
|
||||||
|
.release-description ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-description ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-entry time {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-message {
|
.page-message {
|
||||||
|
|||||||
@@ -3,20 +3,25 @@
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||||
|
import { useReleaseCommunicationsStore } from '@/features/release-communications/stores/releaseCommunicationsStore.js';
|
||||||
import WorkspaceSelector from './WorkspaceSelector.vue';
|
import WorkspaceSelector from './WorkspaceSelector.vue';
|
||||||
import {
|
import {
|
||||||
mdiCalendar,
|
mdiCalendar,
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
|
mdiEyeOffOutline,
|
||||||
|
mdiFlagVariantOutline,
|
||||||
mdiFormatListBulleted,
|
mdiFormatListBulleted,
|
||||||
mdiLogin,
|
mdiLogin,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
|
mdiRefresh,
|
||||||
mdiTable,
|
mdiTable,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
const releaseCommunicationsStore = useReleaseCommunicationsStore();
|
||||||
const isContentViewMenuOpen = ref(false);
|
const isContentViewMenuOpen = ref(false);
|
||||||
|
|
||||||
const contentViewActions = computed(() => {
|
const contentViewActions = computed(() => {
|
||||||
@@ -104,6 +109,66 @@
|
|||||||
icon: mdiPlus,
|
icon: mdiPlus,
|
||||||
route: { name: 'channels', query: { create: 'true' } },
|
route: { name: 'channels', query: { create: 'true' } },
|
||||||
}];
|
}];
|
||||||
|
case 'developer-release-notes':
|
||||||
|
return route.query.tab === 'release-notes'
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: 'refresh-release-commits',
|
||||||
|
label: t('releaseCommunications.commits.refresh'),
|
||||||
|
icon: mdiRefresh,
|
||||||
|
route: {
|
||||||
|
name: 'developer-release-notes',
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
tab: 'git-log',
|
||||||
|
refreshCommits: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'exclude-release-commits',
|
||||||
|
label: t('releaseCommunications.commits.exclude'),
|
||||||
|
icon: mdiEyeOffOutline,
|
||||||
|
disabled: releaseCommunicationsStore.selectedCommitShas.length === 0,
|
||||||
|
route: {
|
||||||
|
name: 'developer-release-notes',
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
tab: 'git-log',
|
||||||
|
excludeCommits: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'create-first-release',
|
||||||
|
label: t('releaseCommunications.developer.createFirstRelease'),
|
||||||
|
icon: mdiFlagVariantOutline,
|
||||||
|
disabled: releaseCommunicationsStore.selectedCommitShas.length !== 1,
|
||||||
|
route: {
|
||||||
|
name: 'developer-release-notes',
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
tab: 'git-log',
|
||||||
|
createFirstRelease: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'create-release-note',
|
||||||
|
label: t('releaseCommunications.developer.createReleaseNote'),
|
||||||
|
icon: mdiPlus,
|
||||||
|
disabled: releaseCommunicationsStore.selectedCommitShas.length === 0,
|
||||||
|
route: {
|
||||||
|
name: 'developer-release-notes',
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
tab: 'git-log',
|
||||||
|
createReleaseNote: 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
case 'workspace-settings':
|
case 'workspace-settings':
|
||||||
case 'settings-user-information':
|
case 'settings-user-information':
|
||||||
case 'settings-workspaces':
|
case 'settings-workspaces':
|
||||||
@@ -179,9 +244,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link
|
<template
|
||||||
v-for="action in appBarActions"
|
v-for="action in appBarActions"
|
||||||
:key="action.key"
|
:key="action.key"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="action.disabled"
|
||||||
|
class="menu-item-action"
|
||||||
|
type="button"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<v-icon :icon="action.icon" />
|
||||||
|
<span class="label">{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
:to="action.route"
|
:to="action.route"
|
||||||
class="menu-action-link"
|
class="menu-action-link"
|
||||||
>
|
>
|
||||||
@@ -190,6 +267,7 @@
|
|||||||
<span class="label">{{ action.label }}</span>
|
<span class="label">{{ action.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -256,6 +334,16 @@
|
|||||||
color: #fffaf2;
|
color: #fffaf2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-item-action:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-action:disabled:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
.view-selector-option {
|
.view-selector-option {
|
||||||
@apply flex min-h-11 w-full items-center gap-3 rounded-[0.75rem] px-3 text-left text-sm font-semibold transition;
|
@apply flex min-h-11 w-full items-center gap-3 rounded-[0.75rem] px-3 text-left text-sm font-semibold transition;
|
||||||
color: #172033;
|
color: #172033;
|
||||||
|
|||||||
@@ -52,17 +52,20 @@
|
|||||||
const collapsedSearchInputRef = ref(null);
|
const collapsedSearchInputRef = ref(null);
|
||||||
const collapsedSearchPanelStyle = ref({});
|
const collapsedSearchPanelStyle = ref({});
|
||||||
|
|
||||||
|
const filterVisibleLinks = links =>
|
||||||
|
links.filter(link => !link.roles || authStore.hasAnyRole(link.roles));
|
||||||
|
|
||||||
const primaryLinks = [
|
const primaryLinks = [
|
||||||
{ to: '/app/dashboard', labelKey: 'nav.overview', icon: mdiHomeOutline },
|
{ to: '/app/dashboard', labelKey: 'nav.overview', icon: mdiHomeOutline },
|
||||||
{ to: '/app/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
|
{ to: '/app/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
|
||||||
|
{ to: '/app/developer/release-notes', labelKey: 'nav.releaseNotes', icon: mdiSourceCommit, roles: ['developer'], badge: 'commits' },
|
||||||
|
];
|
||||||
|
const bottomLinks = [
|
||||||
{ to: '/app/updates', labelKey: 'nav.whatsNew', icon: mdiBullhornOutline, badge: 'updates' },
|
{ to: '/app/updates', labelKey: 'nav.whatsNew', icon: mdiBullhornOutline, badge: 'updates' },
|
||||||
{ to: '/app/feedback', labelKey: 'nav.feedbackReview', icon: mdiBugOutline, roles: ['developer'] },
|
{ to: '/app/feedback', labelKey: 'nav.feedbackReview', icon: mdiBugOutline, roles: ['developer'] },
|
||||||
{ to: '/app/developer/updates', labelKey: 'nav.releaseUpdates', icon: mdiBullhornOutline, roles: ['developer'] },
|
|
||||||
{ to: '/app/developer/release-commits', labelKey: 'nav.releaseCommits', icon: mdiSourceCommit, roles: ['developer'], badge: 'commits' },
|
|
||||||
];
|
];
|
||||||
const visiblePrimaryLinks = computed(() =>
|
const visiblePrimaryLinks = computed(() => filterVisibleLinks(primaryLinks));
|
||||||
primaryLinks.filter(link => !link.roles || authStore.hasAnyRole(link.roles))
|
const visibleBottomLinks = computed(() => filterVisibleLinks(bottomLinks));
|
||||||
);
|
|
||||||
|
|
||||||
const openSections = ref({
|
const openSections = ref({
|
||||||
channels: false,
|
channels: false,
|
||||||
@@ -305,11 +308,14 @@
|
|||||||
:title="!isExpanded ? 'Search' : null"
|
:title="!isExpanded ? 'Search' : null"
|
||||||
@click="openCollapsedSearch"
|
@click="openCollapsedSearch"
|
||||||
>
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiMagnify"
|
||||||
|
class="sidebar-search-icon"
|
||||||
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="isExpanded"
|
v-if="isExpanded"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
class="sidebar-search-input"
|
class="sidebar-search-input"
|
||||||
:prepend-inner-icon="mdiMagnify"
|
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
density="compact"
|
density="compact"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -329,11 +335,14 @@
|
|||||||
v-if="!isExpanded"
|
v-if="!isExpanded"
|
||||||
class="sidebar-search sidebar-search-panel-input"
|
class="sidebar-search sidebar-search-panel-input"
|
||||||
>
|
>
|
||||||
|
<v-icon
|
||||||
|
:icon="mdiMagnify"
|
||||||
|
class="sidebar-search-icon"
|
||||||
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
ref="collapsedSearchInputRef"
|
ref="collapsedSearchInputRef"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
class="sidebar-search-input"
|
class="sidebar-search-input"
|
||||||
:prepend-inner-icon="mdiMagnify"
|
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
density="compact"
|
density="compact"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -456,7 +465,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-section">
|
<div class="sidebar-section sidebar-primary-links">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="link in visiblePrimaryLinks"
|
v-for="link in visiblePrimaryLinks"
|
||||||
:key="link.to"
|
:key="link.to"
|
||||||
@@ -655,6 +664,36 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="authStore.isAuthenticated && visibleBottomLinks.length"
|
||||||
|
class="sidebar-section sidebar-bottom-links"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-for="link in visibleBottomLinks"
|
||||||
|
:key="link.to"
|
||||||
|
:to="link.to"
|
||||||
|
class="sidebar-link"
|
||||||
|
active-class="sidebar-link-active"
|
||||||
|
:title="!isExpanded ? t(link.labelKey) : null"
|
||||||
|
>
|
||||||
|
<span class="sidebar-link-icon-wrap">
|
||||||
|
<v-icon :icon="link.icon" />
|
||||||
|
<span
|
||||||
|
v-if="link.badge === 'updates' && releaseCommunicationsStore.unreadCount"
|
||||||
|
class="sidebar-notification-badge"
|
||||||
|
>
|
||||||
|
{{ Math.min(releaseCommunicationsStore.unreadCount, 9) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="isExpanded"
|
||||||
|
class="sidebar-link-label"
|
||||||
|
>
|
||||||
|
{{ t(link.labelKey) }}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SidebarUserMenu
|
<SidebarUserMenu
|
||||||
v-if="authStore.isAuthenticated"
|
v-if="authStore.isAuthenticated"
|
||||||
:is-expanded="isExpanded"
|
:is-expanded="isExpanded"
|
||||||
@@ -724,7 +763,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-utilities {
|
.sidebar-utilities {
|
||||||
@apply gap-3 pb-1;
|
@apply gap-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-primary-links {
|
||||||
|
margin-top: -0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-search-wrap,
|
.sidebar-search-wrap,
|
||||||
@@ -747,6 +790,7 @@
|
|||||||
|
|
||||||
.sidebar-search-icon {
|
.sidebar-search-icon {
|
||||||
@apply h-5 w-5 flex-shrink-0 text-xl;
|
@apply h-5 w-5 flex-shrink-0 text-xl;
|
||||||
|
color: #526178;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-search-input {
|
.sidebar-search-input {
|
||||||
@@ -755,6 +799,10 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-search-input :deep(.v-field__input) {
|
||||||
|
@apply min-h-0 p-0;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-search-input::placeholder {
|
.sidebar-search-input::placeholder {
|
||||||
color: #7a8799;
|
color: #7a8799;
|
||||||
}
|
}
|
||||||
@@ -866,6 +914,10 @@
|
|||||||
@apply flex flex-col gap-2;
|
@apply flex flex-col gap-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-bottom-links {
|
||||||
|
@apply flex-shrink-0 pb-4;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-section-header {
|
.sidebar-section-header {
|
||||||
@apply flex items-center gap-2;
|
@apply flex items-center gap-2;
|
||||||
}
|
}
|
||||||
@@ -916,6 +968,7 @@
|
|||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-search :deep(.v-icon),
|
||||||
.sidebar-link :deep(.v-icon),
|
.sidebar-link :deep(.v-icon),
|
||||||
.sidebar-section-action :deep(.v-icon) {
|
.sidebar-section-action :deep(.v-icon) {
|
||||||
@apply h-5 w-5 flex-shrink-0 text-xl;
|
@apply h-5 w-5 flex-shrink-0 text-xl;
|
||||||
|
|||||||
@@ -562,7 +562,7 @@
|
|||||||
"feedbackReview": "Feedback Review",
|
"feedbackReview": "Feedback Review",
|
||||||
"whatsNew": "What's New",
|
"whatsNew": "What's New",
|
||||||
"releaseUpdates": "Release Updates",
|
"releaseUpdates": "Release Updates",
|
||||||
"releaseCommits": "Release Commits",
|
"releaseNotes": "Release Notes",
|
||||||
"channels": "Channels",
|
"channels": "Channels",
|
||||||
"campaigns": "Campaigns",
|
"campaigns": "Campaigns",
|
||||||
"reviewQueue": "Review Queue",
|
"reviewQueue": "Review Queue",
|
||||||
@@ -597,6 +597,14 @@
|
|||||||
},
|
},
|
||||||
"releaseCommunications": {
|
"releaseCommunications": {
|
||||||
"summary": "Summary",
|
"summary": "Summary",
|
||||||
|
"description": "Description",
|
||||||
|
"noteTitle": "Title",
|
||||||
|
"english": "English",
|
||||||
|
"french": "French",
|
||||||
|
"titleEn": "English title",
|
||||||
|
"descriptionEn": "English description",
|
||||||
|
"titleFr": "French title",
|
||||||
|
"descriptionFr": "French description",
|
||||||
"body": "Body",
|
"body": "Body",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"importance": "Importance",
|
"importance": "Importance",
|
||||||
@@ -618,44 +626,59 @@
|
|||||||
"developer": {
|
"developer": {
|
||||||
"eyebrow": "SaaS operator",
|
"eyebrow": "SaaS operator",
|
||||||
"title": "Release updates",
|
"title": "Release updates",
|
||||||
|
"description": "Create, publish, and archive curated product updates.",
|
||||||
|
"creationTitle": "Release Note",
|
||||||
|
"creationDescription": "Draft a release note from selected commits or edit an existing release.",
|
||||||
|
"createReleaseNote": "Create Release Note",
|
||||||
|
"createFirstRelease": "Create First Release",
|
||||||
|
"pastReleases": "Past releases",
|
||||||
|
"pastReleasesDescription": "Published, archived, and draft release notes.",
|
||||||
|
"noReleaseNotes": "No release notes yet.",
|
||||||
"newUpdate": "New update",
|
"newUpdate": "New update",
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
"pushEmail": "Push email",
|
|
||||||
"testMode": "Send to me only",
|
|
||||||
"confirmResend": "Confirm resend",
|
|
||||||
"sendEmail": "Send email",
|
|
||||||
"confirmEmail": "Send this release update by email?",
|
|
||||||
"emailResult": "Email sent to {count} recipient(s).",
|
|
||||||
"linkedCommits": "Linked commits",
|
"linkedCommits": "Linked commits",
|
||||||
"noLinkedCommits": "No commits linked to this update yet."
|
"noLinkedCommits": "No commits linked to this update yet."
|
||||||
},
|
},
|
||||||
"commits": {
|
"commits": {
|
||||||
"eyebrow": "SaaS operator",
|
"eyebrow": "SaaS operator",
|
||||||
"title": "Release commits",
|
"title": "Release notes",
|
||||||
"description": "Import shipped commits and reconcile them with curated update entries.",
|
"description": "Review pending commits and maintain the release note history.",
|
||||||
|
"gitLogTab": "Changes",
|
||||||
|
"releaseNotesTab": "History",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"exclude": "Exclude",
|
||||||
"unreviewed": "unreviewed",
|
"unreviewed": "unreviewed",
|
||||||
"branch": "Branch or ref",
|
"branch": "Branch or ref",
|
||||||
"limit": "Limit",
|
"limit": "Limit",
|
||||||
"since": "Since",
|
"since": "Since",
|
||||||
"until": "Until",
|
"until": "Until",
|
||||||
"fetch": "Fetch commits",
|
"fetch": "Fetch commits",
|
||||||
"importJson": "Commit JSON payload",
|
|
||||||
"import": "Import commits",
|
|
||||||
"importResult": "Imported {imported}, updated {updated}, skipped {skipped}.",
|
"importResult": "Imported {imported}, updated {updated}, skipped {skipped}.",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"linkedUpdate": "Linked update",
|
"linkedUpdate": "Linked update",
|
||||||
|
"releaseNote": "Release note",
|
||||||
|
"notIncluded": "Not included",
|
||||||
|
"inclusion": {
|
||||||
|
"label": "Release note status",
|
||||||
|
"notIncluded": "Not in a release note",
|
||||||
|
"included": "In a release note",
|
||||||
|
"all": "All commits"
|
||||||
|
},
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
|
"sha": "SHA",
|
||||||
|
"commit": "Commit",
|
||||||
|
"committed": "Committed",
|
||||||
|
"actions": "Actions",
|
||||||
|
"empty": "No commits match the current filters.",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"link": "Update",
|
"link": "Update",
|
||||||
"selected": "{count} selected",
|
"selected": "{count} selected",
|
||||||
"selectCommit": "Select commit",
|
"selectCommit": "Select commit",
|
||||||
"copy": "Copy",
|
|
||||||
"copySelected": "Copy selected",
|
|
||||||
"copied": "Copied.",
|
"copied": "Copied.",
|
||||||
"clearSelection": "Clear selection",
|
"createUpdate": "Create release note",
|
||||||
"createUpdate": "Create update entry",
|
"selectedCommits": "Selected commits",
|
||||||
"internalOnly": "Internal only",
|
"internalOnly": "Internal only",
|
||||||
"ignore": "Ignore"
|
"ignore": "Ignore"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,7 +562,7 @@
|
|||||||
"feedbackReview": "Revue feedback",
|
"feedbackReview": "Revue feedback",
|
||||||
"whatsNew": "Nouveautés",
|
"whatsNew": "Nouveautés",
|
||||||
"releaseUpdates": "Mises à jour",
|
"releaseUpdates": "Mises à jour",
|
||||||
"releaseCommits": "Commits release",
|
"releaseNotes": "Notes de version",
|
||||||
"channels": "Canaux",
|
"channels": "Canaux",
|
||||||
"campaigns": "Campagnes",
|
"campaigns": "Campagnes",
|
||||||
"reviewQueue": "File de révision",
|
"reviewQueue": "File de révision",
|
||||||
@@ -597,6 +597,14 @@
|
|||||||
},
|
},
|
||||||
"releaseCommunications": {
|
"releaseCommunications": {
|
||||||
"summary": "Résumé",
|
"summary": "Résumé",
|
||||||
|
"description": "Description",
|
||||||
|
"noteTitle": "Titre",
|
||||||
|
"english": "Anglais",
|
||||||
|
"french": "Français",
|
||||||
|
"titleEn": "Titre anglais",
|
||||||
|
"descriptionEn": "Description anglaise",
|
||||||
|
"titleFr": "Titre français",
|
||||||
|
"descriptionFr": "Description française",
|
||||||
"body": "Détail",
|
"body": "Détail",
|
||||||
"category": "Catégorie",
|
"category": "Catégorie",
|
||||||
"importance": "Importance",
|
"importance": "Importance",
|
||||||
@@ -618,44 +626,59 @@
|
|||||||
"developer": {
|
"developer": {
|
||||||
"eyebrow": "Opérateur SaaS",
|
"eyebrow": "Opérateur SaaS",
|
||||||
"title": "Mises à jour release",
|
"title": "Mises à jour release",
|
||||||
|
"description": "Créez, publiez et archivez les mises à jour produit rédigées.",
|
||||||
|
"creationTitle": "Note de release",
|
||||||
|
"creationDescription": "Rédigez une note depuis les commits sélectionnés ou modifiez une release existante.",
|
||||||
|
"createReleaseNote": "Créer une note de release",
|
||||||
|
"createFirstRelease": "Créer la première release",
|
||||||
|
"pastReleases": "Releases passées",
|
||||||
|
"pastReleasesDescription": "Notes de release publiées, archivées et brouillons.",
|
||||||
|
"noReleaseNotes": "Aucune note de release pour le moment.",
|
||||||
"newUpdate": "Nouvelle mise à jour",
|
"newUpdate": "Nouvelle mise à jour",
|
||||||
"publish": "Publier",
|
"publish": "Publier",
|
||||||
"archive": "Archiver",
|
"archive": "Archiver",
|
||||||
"pushEmail": "Email push",
|
|
||||||
"testMode": "M'envoyer seulement",
|
|
||||||
"confirmResend": "Confirmer le renvoi",
|
|
||||||
"sendEmail": "Envoyer email",
|
|
||||||
"confirmEmail": "Envoyer cette mise à jour par email?",
|
|
||||||
"emailResult": "Email envoyé à {count} destinataire(s).",
|
|
||||||
"linkedCommits": "Commits liés",
|
"linkedCommits": "Commits liés",
|
||||||
"noLinkedCommits": "Aucun commit lié à cette mise à jour."
|
"noLinkedCommits": "Aucun commit lié à cette mise à jour."
|
||||||
},
|
},
|
||||||
"commits": {
|
"commits": {
|
||||||
"eyebrow": "Opérateur SaaS",
|
"eyebrow": "Opérateur SaaS",
|
||||||
"title": "Commits release",
|
"title": "Notes de release",
|
||||||
"description": "Importez les commits livrés et associez-les aux mises à jour rédigées.",
|
"description": "Révisez les commits en attente et gérez l'historique des notes de release.",
|
||||||
|
"gitLogTab": "Changements",
|
||||||
|
"releaseNotesTab": "Historique",
|
||||||
|
"refresh": "Actualiser",
|
||||||
|
"exclude": "Exclure",
|
||||||
"unreviewed": "non révisés",
|
"unreviewed": "non révisés",
|
||||||
"branch": "Branche ou ref",
|
"branch": "Branche ou ref",
|
||||||
"limit": "Limite",
|
"limit": "Limite",
|
||||||
"since": "Depuis",
|
"since": "Depuis",
|
||||||
"until": "Jusqu'au",
|
"until": "Jusqu'au",
|
||||||
"fetch": "Récupérer commits",
|
"fetch": "Récupérer commits",
|
||||||
"importJson": "Payload JSON de commits",
|
|
||||||
"import": "Importer commits",
|
|
||||||
"importResult": "{imported} importés, {updated} mis à jour, {skipped} ignorés.",
|
"importResult": "{imported} importés, {updated} mis à jour, {skipped} ignorés.",
|
||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"linkedUpdate": "Mise à jour liée",
|
"linkedUpdate": "Mise à jour liée",
|
||||||
|
"releaseNote": "Note de release",
|
||||||
|
"notIncluded": "Non inclus",
|
||||||
|
"inclusion": {
|
||||||
|
"label": "Statut de note",
|
||||||
|
"notIncluded": "Non inclus dans une note",
|
||||||
|
"included": "Inclus dans une note",
|
||||||
|
"all": "Tous les commits"
|
||||||
|
},
|
||||||
"author": "Auteur",
|
"author": "Auteur",
|
||||||
|
"sha": "SHA",
|
||||||
|
"commit": "Commit",
|
||||||
|
"committed": "Commité",
|
||||||
|
"actions": "Actions",
|
||||||
|
"empty": "Aucun commit ne correspond aux filtres actuels.",
|
||||||
"clear": "Effacer",
|
"clear": "Effacer",
|
||||||
"link": "Mise à jour",
|
"link": "Mise à jour",
|
||||||
"selected": "{count} sélectionnés",
|
"selected": "{count} sélectionnés",
|
||||||
"selectCommit": "Sélectionner le commit",
|
"selectCommit": "Sélectionner le commit",
|
||||||
"copy": "Copier",
|
|
||||||
"copySelected": "Copier sélection",
|
|
||||||
"copied": "Copié.",
|
"copied": "Copié.",
|
||||||
"clearSelection": "Effacer sélection",
|
"createUpdate": "Créer la note de release",
|
||||||
"createUpdate": "Créer une entrée",
|
"selectedCommits": "Commits sélectionnés",
|
||||||
"internalOnly": "Interne seulement",
|
"internalOnly": "Interne seulement",
|
||||||
"ignore": "Ignorer"
|
"ignore": "Ignorer"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,17 @@ import { createApp } from 'vue';
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from '@/router/router.js';
|
import router from '@/router/router.js';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
import './assets/main.css';
|
||||||
import 'vuetify/styles';
|
import 'vuetify/styles';
|
||||||
import { createVuetify } from 'vuetify';
|
import { createVuetify } from 'vuetify';
|
||||||
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
|
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg';
|
||||||
import {
|
import * as components from 'vuetify/components';
|
||||||
VAlert,
|
import * as directives from 'vuetify/directives';
|
||||||
VApp,
|
|
||||||
VBtn,
|
|
||||||
VCheckbox,
|
|
||||||
VCheckboxBtn,
|
|
||||||
VDialog,
|
|
||||||
VFileInput,
|
|
||||||
VForm,
|
|
||||||
VIcon,
|
|
||||||
VProgressCircular,
|
|
||||||
VProgressLinear,
|
|
||||||
VRadio,
|
|
||||||
VRadioGroup,
|
|
||||||
VSelect,
|
|
||||||
VSnackbar,
|
|
||||||
VTextarea,
|
|
||||||
VTextField,
|
|
||||||
} from 'vuetify/components';
|
|
||||||
import vueGoogleOauth from 'vue3-google-login';
|
import vueGoogleOauth from 'vue3-google-login';
|
||||||
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
import { useAuthStore } from '@/features/auth/stores/authStore.js';
|
||||||
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
|
||||||
import Toast, { POSITION } from 'vue-toastification';
|
import Toast, { POSITION } from 'vue-toastification';
|
||||||
import 'vue-toastification/dist/index.css';
|
import 'vue-toastification/dist/index.css';
|
||||||
import './assets/main.css';
|
|
||||||
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
||||||
import { useReviewQueueStore } from '@/features/reviews/stores/reviewQueueStore.js';
|
import { useReviewQueueStore } from '@/features/reviews/stores/reviewQueueStore.js';
|
||||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||||
@@ -44,26 +27,8 @@ import { createHead } from '@vueuse/head';
|
|||||||
import { socializeTheme } from '@/plugins/theme.js';
|
import { socializeTheme } from '@/plugins/theme.js';
|
||||||
|
|
||||||
const vuetify = createVuetify({
|
const vuetify = createVuetify({
|
||||||
components: {
|
components,
|
||||||
VDialog,
|
directives,
|
||||||
VApp,
|
|
||||||
VBtn,
|
|
||||||
VCheckbox,
|
|
||||||
VCheckboxBtn,
|
|
||||||
VFileInput,
|
|
||||||
VProgressLinear,
|
|
||||||
VProgressCircular,
|
|
||||||
VIcon,
|
|
||||||
VRadio,
|
|
||||||
VRadioGroup,
|
|
||||||
VSelect,
|
|
||||||
VTextField,
|
|
||||||
VSnackbar,
|
|
||||||
VForm,
|
|
||||||
VTextarea,
|
|
||||||
VAlert,
|
|
||||||
},
|
|
||||||
directives: {},
|
|
||||||
icons: {
|
icons: {
|
||||||
defaultSet: 'mdi',
|
defaultSet: 'mdi',
|
||||||
aliases,
|
aliases,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const MyFeedbackDetailView = () => import('@/features/feedback/views/MyFeedbackD
|
|||||||
const DeveloperFeedbackListView = () => import('@/features/feedback/views/DeveloperFeedbackListView.vue');
|
const DeveloperFeedbackListView = () => import('@/features/feedback/views/DeveloperFeedbackListView.vue');
|
||||||
const DeveloperFeedbackDetailView = () => import('@/features/feedback/views/DeveloperFeedbackDetailView.vue');
|
const DeveloperFeedbackDetailView = () => import('@/features/feedback/views/DeveloperFeedbackDetailView.vue');
|
||||||
const UpdatesView = () => import('@/features/release-communications/views/UpdatesView.vue');
|
const UpdatesView = () => import('@/features/release-communications/views/UpdatesView.vue');
|
||||||
const DeveloperUpdatesView = () => import('@/features/release-communications/views/DeveloperUpdatesView.vue');
|
|
||||||
const DeveloperReleaseCommitsView = () => import('@/features/release-communications/views/DeveloperReleaseCommitsView.vue');
|
const DeveloperReleaseCommitsView = () => import('@/features/release-communications/views/DeveloperReleaseCommitsView.vue');
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@@ -135,12 +134,17 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/app/developer/updates',
|
path: '/app/developer/updates',
|
||||||
name: 'developer-release-updates',
|
name: 'developer-release-updates',
|
||||||
component: DeveloperUpdatesView,
|
redirect: { name: 'developer-release-notes' },
|
||||||
meta: { requiresAuth: true, roles: ['developer'] },
|
meta: { requiresAuth: true, roles: ['developer'] },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/app/developer/release-commits',
|
path: '/app/developer/release-commits',
|
||||||
name: 'developer-release-commits',
|
redirect: { name: 'developer-release-notes' },
|
||||||
|
meta: { requiresAuth: true, roles: ['developer'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/app/developer/release-notes',
|
||||||
|
name: 'developer-release-notes',
|
||||||
component: DeveloperReleaseCommitsView,
|
component: DeveloperReleaseCommitsView,
|
||||||
meta: { requiresAuth: true, roles: ['developer'] },
|
meta: { requiresAuth: true, roles: ['developer'] },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"url": "http://127.0.0.1:5080"
|
"url": "http://localhost:5080"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
@@ -659,53 +659,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/developer/release-commits/import": {
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"Release Communications",
|
|
||||||
"Api"
|
|
||||||
],
|
|
||||||
"operationId": "SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsHandler",
|
|
||||||
"requestBody": {
|
|
||||||
"x-name": "ImportDeveloperReleaseCommitsRequest",
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true,
|
|
||||||
"x-position": 1
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Success",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseCommitImportResultDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"JWTBearerAuth": [
|
|
||||||
"developer"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/developer/release-commits": {
|
"/api/developer/release-commits": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -873,43 +826,20 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/developer/release-updates/{id}/send-email": {
|
"/api/developer/release-commits/refresh": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
"Release Communications",
|
"Release Communications",
|
||||||
"Api"
|
"Api"
|
||||||
],
|
],
|
||||||
"operationId": "SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailHandler",
|
"operationId": "SocializeApiModulesReleaseCommunicationsHandlersRefreshDeveloperReleaseCommitsHandler",
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"x-name": "SendDeveloperReleaseUpdateEmailRequest",
|
|
||||||
"description": "",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true,
|
|
||||||
"x-position": 1
|
|
||||||
},
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Success",
|
"description": "Success",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateEmailSendResultDto"
|
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseCommitRefreshResultDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -987,6 +917,63 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/developer/release-commits/{sha}/link-first-release": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Release Communications",
|
||||||
|
"Api"
|
||||||
|
],
|
||||||
|
"operationId": "SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsHandler",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "sha",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"x-name": "LinkFirstReleaseCommitsRequest",
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-position": 1
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseCommitBulkLinkResultDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWTBearerAuth": [
|
||||||
|
"developer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/developer/release-commits/{sha}/unlink": {
|
"/api/developer/release-commits/{sha}/unlink": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -4946,37 +4933,24 @@
|
|||||||
"title": {
|
"title": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"summary": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"body": {
|
"titleEn": {
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"importance": {
|
"descriptionEn": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"audience": {
|
"titleFr": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"descriptionFr": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"deploymentLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"buildVersion": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"commitRange": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time"
|
"format": "date-time"
|
||||||
@@ -4995,25 +4969,6 @@
|
|||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
"manualEmailSentByUserId": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "guid",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"manualEmailSentAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"manualEmailAudience": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"manualEmailRecipientCount": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"isRead": {
|
"isRead": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
@@ -5023,66 +4978,35 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"title",
|
"titleEn",
|
||||||
"summary",
|
"descriptionEn",
|
||||||
"category",
|
"titleFr",
|
||||||
"importance",
|
"descriptionFr"
|
||||||
"audience"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {
|
"titleEn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 160,
|
"maxLength": 160,
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
"summary": {
|
"descriptionEn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 512,
|
"maxLength": 4000,
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
"body": {
|
"titleFr": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 8000,
|
"maxLength": 160,
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 32,
|
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
"importance": {
|
"descriptionFr": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 32,
|
"maxLength": 4000,
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
|
||||||
"audience": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 32,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": false
|
|
||||||
},
|
|
||||||
"deploymentLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 128,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"buildVersion": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 128,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"commitRange": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 256,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5106,30 +5030,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SocializeApiModulesReleaseCommunicationsContractsReleaseCommitImportResultDto": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"importedCount": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"updatedCount": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"skippedCount": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"commits": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto": {
|
"SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -5191,108 +5091,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitsRequest": {
|
"SocializeApiModulesReleaseCommunicationsContractsReleaseCommitRefreshResultDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"sinceSha": {
|
"createdCount": {
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"untilSha": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"sourceBranch": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"deploymentLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"commits": {
|
|
||||||
"type": "array",
|
|
||||||
"nullable": true,
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SocializeApiModulesReleaseCommunicationsHandlersImportDeveloperReleaseCommitDto": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"sha": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"shortSha": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"subject": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"authorName": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"authorEmail": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"authoredAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"committedAt": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "date-time",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"sourceBranch": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"deploymentLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"externalUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateEmailSendResultDto": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"recipientCount": {
|
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
},
|
},
|
||||||
"sentAt": {
|
"updatedCount": {
|
||||||
"type": "string",
|
"type": "integer",
|
||||||
"format": "date-time"
|
"format": "int32"
|
||||||
},
|
},
|
||||||
"testMode": {
|
"skippedCount": {
|
||||||
"type": "boolean"
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"commits": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseCommitDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"SocializeApiModulesReleaseCommunicationsHandlersSendDeveloperReleaseUpdateEmailRequest": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"testMode": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"confirmResend": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SocializeApiModulesReleaseCommunicationsHandlersLinkDeveloperReleaseCommitRequest": {
|
"SocializeApiModulesReleaseCommunicationsHandlersLinkDeveloperReleaseCommitRequest": {
|
||||||
@@ -5305,70 +5125,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SocializeApiModulesReleaseCommunicationsContractsReleaseCommitBulkLinkResultDto": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"linkedCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SocializeApiModulesReleaseCommunicationsHandlersLinkFirstReleaseCommitsRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"releaseUpdateId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "guid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SocializeApiModulesReleaseCommunicationsHandlersUpdateDeveloperReleaseUpdateRequest": {
|
"SocializeApiModulesReleaseCommunicationsHandlersUpdateDeveloperReleaseUpdateRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"title",
|
"titleEn",
|
||||||
"summary",
|
"descriptionEn",
|
||||||
"category",
|
"titleFr",
|
||||||
"importance",
|
"descriptionFr"
|
||||||
"audience"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {
|
"titleEn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 160,
|
"maxLength": 160,
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
"summary": {
|
"descriptionEn": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 512,
|
"maxLength": 4000,
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
"body": {
|
"titleFr": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 8000,
|
"maxLength": 160,
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"category": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 32,
|
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
},
|
||||||
"importance": {
|
"descriptionFr": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 32,
|
"maxLength": 4000,
|
||||||
"minLength": 0,
|
"minLength": 0,
|
||||||
"nullable": false
|
"nullable": false
|
||||||
},
|
|
||||||
"audience": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 32,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": false
|
|
||||||
},
|
|
||||||
"deploymentLabel": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 128,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"buildVersion": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 128,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"commitRange": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 256,
|
|
||||||
"minLength": 0,
|
|
||||||
"nullable": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user