From 8101256d4a32af456fec484ffb711f79a4717947 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 17 Apr 2024 14:04:58 +0200 Subject: [PATCH] Add project confidentiality --- backend/LexBoxApi/GraphQL/ProjectMutations.cs | 19 + .../Models/Project/ChangeProjectInputs.cs | 2 + .../Models/Project/CreateProjectInput.cs | 1 + backend/LexBoxApi/Services/ProjectService.cs | 7 + backend/LexCore/Entities/DraftProject.cs | 1 + backend/LexCore/Entities/Project.cs | 1 + ...ColumnToProjectAndDraftProject.Designer.cs | 857 ++++++++++++++++++ ...fidentialColumnToProjectAndDraftProject.cs | 38 + .../LexBoxDbContextModelSnapshot.cs | 6 + backend/LexData/SeedingData.cs | 4 +- .../LexCore/Services/ProjectServiceTest.cs | 6 +- frontend/schema.graphql | 20 + frontend/src/lib/app.postcss | 4 +- .../src/lib/components/Badges/Badge.svelte | 10 +- .../lib/components/Badges/BadgeButton.svelte | 7 +- .../src/lib/components/EditableText.svelte | 2 +- .../src/lib/components/MoreSettings.svelte | 4 +- .../ProjectConfidentialityCombobox.svelte | 25 + .../ProjectConfidentialityFilterSelect.svelte | 22 + .../components/Projects/ProjectFilter.svelte | 33 +- .../components/Projects/ProjectTable.svelte | 33 +- frontend/src/lib/components/Projects/index.ts | 1 + .../src/lib/email/CreateProjectRequest.svelte | 5 +- frontend/src/lib/forms/Checkbox.svelte | 18 +- frontend/src/lib/i18n/locales/en.json | 20 + frontend/src/lib/type.utils.ts | 5 + frontend/src/lib/util/query-params.ts | 6 +- .../src/routes/(authenticated)/+page.svelte | 2 +- frontend/src/routes/(authenticated)/+page.ts | 1 + .../routes/(authenticated)/admin/+page.svelte | 3 + .../src/routes/(authenticated)/admin/+page.ts | 4 + .../admin/AdminProjects.svelte | 5 +- .../project/[project_code]/+page.svelte | 113 ++- .../project/[project_code]/+page.ts | 47 +- .../[project_code]/OpenInFlexModal.svelte | 6 +- .../ProjectConfidentialityBadge.svelte | 26 + .../ProjectConfidentialityModal.svelte | 51 ++ .../project/create/+page.svelte | 9 + .../src/routes/email/tester/+page@.svelte | 7 +- frontend/tests/fixtures.ts | 3 +- 40 files changed, 1328 insertions(+), 106 deletions(-) create mode 100644 backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.Designer.cs create mode 100644 backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.cs create mode 100644 frontend/src/lib/components/Projects/ProjectConfidentialityCombobox.svelte create mode 100644 frontend/src/lib/components/Projects/ProjectConfidentialityFilterSelect.svelte create mode 100644 frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityBadge.svelte create mode 100644 frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityModal.svelte diff --git a/backend/LexBoxApi/GraphQL/ProjectMutations.cs b/backend/LexBoxApi/GraphQL/ProjectMutations.cs index 36959f5c7..49ba58e5e 100644 --- a/backend/LexBoxApi/GraphQL/ProjectMutations.cs +++ b/backend/LexBoxApi/GraphQL/ProjectMutations.cs @@ -278,6 +278,25 @@ public async Task> ChangeProjectDescription(ChangeProjectDes return dbContext.Projects.Where(p => p.Id == input.ProjectId); } + [Error] + [Error] + [UseMutationConvention] + [UseFirstOrDefault] + [UseProjection] + public async Task> SetProjectConfidentiality(SetProjectConfidentialityInput input, + IPermissionService permissionService, + LexBoxDbContext dbContext) + { + permissionService.AssertCanManageProject(input.ProjectId); + var project = await dbContext.Projects.FindAsync(input.ProjectId); + NotFoundException.ThrowIfNull(project); + + project.IsConfidential = input.IsConfidential; + project.UpdateUpdatedDate(); + await dbContext.SaveChangesAsync(); + return dbContext.Projects.Where(p => p.Id == input.ProjectId); + } + [Error] [Error] [UseMutationConvention] diff --git a/backend/LexBoxApi/Models/Project/ChangeProjectInputs.cs b/backend/LexBoxApi/Models/Project/ChangeProjectInputs.cs index 212ebad46..d6fed8c71 100644 --- a/backend/LexBoxApi/Models/Project/ChangeProjectInputs.cs +++ b/backend/LexBoxApi/Models/Project/ChangeProjectInputs.cs @@ -4,6 +4,8 @@ public record ChangeProjectNameInput(Guid ProjectId, string Name); public record ChangeProjectDescriptionInput(Guid ProjectId, string Description); +public record SetProjectConfidentialityInput(Guid ProjectId, bool IsConfidential); + public record DeleteUserByAdminOrSelfInput(Guid UserId); public record ResetProjectByAdminInput(string Code); diff --git a/backend/LexBoxApi/Models/Project/CreateProjectInput.cs b/backend/LexBoxApi/Models/Project/CreateProjectInput.cs index faf215ada..e97c6720d 100644 --- a/backend/LexBoxApi/Models/Project/CreateProjectInput.cs +++ b/backend/LexBoxApi/Models/Project/CreateProjectInput.cs @@ -11,5 +11,6 @@ public record CreateProjectInput( string Code, ProjectType Type, RetentionPolicy RetentionPolicy, + bool IsConfidential, Guid? ProjectManagerId ); diff --git a/backend/LexBoxApi/Services/ProjectService.cs b/backend/LexBoxApi/Services/ProjectService.cs index 86c93679c..db8a3c0f8 100644 --- a/backend/LexBoxApi/Services/ProjectService.cs +++ b/backend/LexBoxApi/Services/ProjectService.cs @@ -17,6 +17,11 @@ public async Task CreateProject(CreateProjectInput input) { await using var transaction = await dbContext.Database.BeginTransactionAsync(); var projectId = input.Id ?? Guid.NewGuid(); + /* TODO #737 - Remove this draftProject/isConfidentialIsUntrustworthy stuff and just trust input.IsConfidential */ + var draftProject = await dbContext.DraftProjects.FindAsync(projectId); + // There could be draft projects from before we introduced the IsConfidential field. (i.e. where draftProject.IsConfidential is null) + // In those cases we can't trust input.IsConfidential == false, because that is the default, but the user will never have had the chance to pick it. + var isConfidentialIsUntrustworthy = draftProject is not null && draftProject.IsConfidential is null && !input.IsConfidential; dbContext.Projects.Add( new Project { @@ -28,6 +33,7 @@ public async Task CreateProject(CreateProjectInput input) Type = input.Type, LastCommit = null, RetentionPolicy = input.RetentionPolicy, + IsConfidential = isConfidentialIsUntrustworthy ? null : input.IsConfidential, Users = input.ProjectManagerId.HasValue ? [new() { UserId = input.ProjectManagerId.Value, Role = ProjectRole.Manager }] : [], }); // Also delete draft project, if any @@ -56,6 +62,7 @@ public async Task CreateDraftProject(CreateProjectInput input) Name = input.Name, Description = input.Description, Type = input.Type, + IsConfidential = input.IsConfidential, RetentionPolicy = input.RetentionPolicy, ProjectManagerId = input.ProjectManagerId, }); diff --git a/backend/LexCore/Entities/DraftProject.cs b/backend/LexCore/Entities/DraftProject.cs index c405aaa57..257bf7eac 100644 --- a/backend/LexCore/Entities/DraftProject.cs +++ b/backend/LexCore/Entities/DraftProject.cs @@ -9,4 +9,5 @@ public class DraftProject : EntityBase public required RetentionPolicy RetentionPolicy { get; set; } public User? ProjectManager { get; set; } public Guid? ProjectManagerId { get; set; } + public required bool? IsConfidential { get; set; } } diff --git a/backend/LexCore/Entities/Project.cs b/backend/LexCore/Entities/Project.cs index 92f46b4f8..5758c0d29 100644 --- a/backend/LexCore/Entities/Project.cs +++ b/backend/LexCore/Entities/Project.cs @@ -15,6 +15,7 @@ public class Project : EntityBase public string? Description { get; set; } public required RetentionPolicy RetentionPolicy { get; set; } public required ProjectType Type { get; set; } + public required bool? IsConfidential { get; set; } public FlexProjectMetadata? FlexProjectMetadata { get; set; } public required List Users { get; set; } public required DateTimeOffset? LastCommit { get; set; } diff --git a/backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.Designer.cs b/backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.Designer.cs new file mode 100644 index 000000000..8dffac4d0 --- /dev/null +++ b/backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.Designer.cs @@ -0,0 +1,857 @@ +// +using System; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace LexData.Migrations +{ + [DbContext(typeof(LexBoxDbContext))] + [Migration("20240425121912_AddIsConfidentialColumnToProjectAndDraftProject")] + partial class AddIsConfidentialColumnToProjectAndDraftProject + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:case_insensitive", "und-u-ks-level2,und-u-ks-level2,icu,False") + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BlobData") + .HasColumnType("bytea") + .HasColumnName("blob_data"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_blob_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCalendar", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Calendar") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("calendar"); + + b.HasKey("SchedulerName", "CalendarName"); + + b.ToTable("qrtz_calendars", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("text") + .HasColumnName("cron_expression"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_cron_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzFiredTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("EntryId") + .HasColumnType("text") + .HasColumnName("entry_id"); + + b.Property("FiredTime") + .HasColumnType("bigint") + .HasColumnName("fired_time"); + + b.Property("InstanceName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.Property("ScheduledTime") + .HasColumnType("bigint") + .HasColumnName("sched_time"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("TriggerGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("TriggerName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.HasKey("SchedulerName", "EntryId"); + + b.HasIndex("InstanceName") + .HasDatabaseName("idx_qrtz_ft_trig_inst_name"); + + b.HasIndex("JobGroup") + .HasDatabaseName("idx_qrtz_ft_job_group"); + + b.HasIndex("JobName") + .HasDatabaseName("idx_qrtz_ft_job_name"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_ft_job_req_recovery"); + + b.HasIndex("TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_group"); + + b.HasIndex("TriggerName") + .HasDatabaseName("idx_qrtz_ft_trig_name"); + + b.HasIndex("SchedulerName", "TriggerName", "TriggerGroup") + .HasDatabaseName("idx_qrtz_ft_trig_nm_gp"); + + b.ToTable("qrtz_fired_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("JobName") + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("JobGroup") + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("IsDurable") + .HasColumnType("bool") + .HasColumnName("is_durable"); + + b.Property("IsNonConcurrent") + .HasColumnType("bool") + .HasColumnName("is_nonconcurrent"); + + b.Property("IsUpdateData") + .HasColumnType("bool") + .HasColumnName("is_update_data"); + + b.Property("JobClassName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_class_name"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("RequestsRecovery") + .HasColumnType("bool") + .HasColumnName("requests_recovery"); + + b.HasKey("SchedulerName", "JobName", "JobGroup"); + + b.HasIndex("RequestsRecovery") + .HasDatabaseName("idx_qrtz_j_req_recovery"); + + b.ToTable("qrtz_job_details", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzLock", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("LockName") + .HasColumnType("text") + .HasColumnName("lock_name"); + + b.HasKey("SchedulerName", "LockName"); + + b.ToTable("qrtz_locks", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzPausedTriggerGroup", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.HasKey("SchedulerName", "TriggerGroup"); + + b.ToTable("qrtz_paused_trigger_grps", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSchedulerState", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("InstanceName") + .HasColumnType("text") + .HasColumnName("instance_name"); + + b.Property("CheckInInterval") + .HasColumnType("bigint") + .HasColumnName("checkin_interval"); + + b.Property("LastCheckInTime") + .HasColumnType("bigint") + .HasColumnName("last_checkin_time"); + + b.HasKey("SchedulerName", "InstanceName"); + + b.ToTable("qrtz_scheduler_state", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("BooleanProperty1") + .HasColumnType("bool") + .HasColumnName("bool_prop_1"); + + b.Property("BooleanProperty2") + .HasColumnType("bool") + .HasColumnName("bool_prop_2"); + + b.Property("DecimalProperty1") + .HasColumnType("numeric") + .HasColumnName("dec_prop_1"); + + b.Property("DecimalProperty2") + .HasColumnType("numeric") + .HasColumnName("dec_prop_2"); + + b.Property("IntegerProperty1") + .HasColumnType("integer") + .HasColumnName("int_prop_1"); + + b.Property("IntegerProperty2") + .HasColumnType("integer") + .HasColumnName("int_prop_2"); + + b.Property("LongProperty1") + .HasColumnType("bigint") + .HasColumnName("long_prop_1"); + + b.Property("LongProperty2") + .HasColumnType("bigint") + .HasColumnName("long_prop_2"); + + b.Property("StringProperty1") + .HasColumnType("text") + .HasColumnName("str_prop_1"); + + b.Property("StringProperty2") + .HasColumnType("text") + .HasColumnName("str_prop_2"); + + b.Property("StringProperty3") + .HasColumnType("text") + .HasColumnName("str_prop_3"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("time_zone_id"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simprop_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("RepeatCount") + .HasColumnType("bigint") + .HasColumnName("repeat_count"); + + b.Property("RepeatInterval") + .HasColumnType("bigint") + .HasColumnName("repeat_interval"); + + b.Property("TimesTriggered") + .HasColumnType("bigint") + .HasColumnName("times_triggered"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.ToTable("qrtz_simple_triggers", "quartz"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Property("SchedulerName") + .HasColumnType("text") + .HasColumnName("sched_name"); + + b.Property("TriggerName") + .HasColumnType("text") + .HasColumnName("trigger_name"); + + b.Property("TriggerGroup") + .HasColumnType("text") + .HasColumnName("trigger_group"); + + b.Property("CalendarName") + .HasColumnType("text") + .HasColumnName("calendar_name"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("bigint") + .HasColumnName("end_time"); + + b.Property("JobData") + .HasColumnType("bytea") + .HasColumnName("job_data"); + + b.Property("JobGroup") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_group"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("MisfireInstruction") + .HasColumnType("smallint") + .HasColumnName("misfire_instr"); + + b.Property("NextFireTime") + .HasColumnType("bigint") + .HasColumnName("next_fire_time"); + + b.Property("PreviousFireTime") + .HasColumnType("bigint") + .HasColumnName("prev_fire_time"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("StartTime") + .HasColumnType("bigint") + .HasColumnName("start_time"); + + b.Property("TriggerState") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_state"); + + b.Property("TriggerType") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trigger_type"); + + b.HasKey("SchedulerName", "TriggerName", "TriggerGroup"); + + b.HasIndex("NextFireTime") + .HasDatabaseName("idx_qrtz_t_next_fire_time"); + + b.HasIndex("TriggerState") + .HasDatabaseName("idx_qrtz_t_state"); + + b.HasIndex("NextFireTime", "TriggerState") + .HasDatabaseName("idx_qrtz_t_nft_st"); + + b.HasIndex("SchedulerName", "JobName", "JobGroup"); + + b.ToTable("qrtz_triggers", "quartz"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectManagerId") + .HasColumnType("uuid"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ProjectManagerId"); + + b.ToTable("DraftProjects"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("LexEntryCount") + .HasColumnType("integer"); + + b.HasKey("ProjectId"); + + b.ToTable("FlexProjectMetadata"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsConfidential") + .HasColumnType("boolean"); + + b.Property("LastCommit") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("ProjectOrigin") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("ResetStatus") + .HasColumnType("integer"); + + b.Property("RetentionPolicy") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("ParentId"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.HasIndex("UserId", "ProjectId") + .IsUnique(); + + b.ToTable("ProjectUsers"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanCreateProjects") + .HasColumnType("boolean"); + + b.Property("CreatedById") + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("GoogleId") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("LastActive") + .HasColumnType("timestamp with time zone"); + + b.Property("LocalizationCode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("en"); + + b.Property("Locked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordStrength") + .HasColumnType("integer"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedDate") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Username") + .HasColumnType("text") + .UseCollation("case_insensitive"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzBlobTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("BlobTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzCronTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("CronTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimplePropertyTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimplePropertyTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzSimpleTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", "Trigger") + .WithMany("SimpleTriggers") + .HasForeignKey("SchedulerName", "TriggerName", "TriggerGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Trigger"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.HasOne("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", "JobDetail") + .WithMany("Triggers") + .HasForeignKey("SchedulerName", "JobName", "JobGroup") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobDetail"); + }); + + modelBuilder.Entity("LexCore.Entities.DraftProject", b => + { + b.HasOne("LexCore.Entities.User", "ProjectManager") + .WithMany() + .HasForeignKey("ProjectManagerId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ProjectManager"); + }); + + modelBuilder.Entity("LexCore.Entities.FlexProjectMetadata", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithOne("FlexProjectMetadata") + .HasForeignKey("LexCore.Entities.FlexProjectMetadata", "ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.HasOne("LexCore.Entities.Project", null) + .WithMany() + .HasForeignKey("ParentId"); + }); + + modelBuilder.Entity("LexCore.Entities.ProjectUsers", b => + { + b.HasOne("LexCore.Entities.Project", "Project") + .WithMany("Users") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LexCore.Entities.User", "User") + .WithMany("Projects") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.HasOne("LexCore.Entities.User", "CreatedBy") + .WithMany("UsersICreated") + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzJobDetail", b => + { + b.Navigation("Triggers"); + }); + + modelBuilder.Entity("AppAny.Quartz.EntityFrameworkCore.Migrations.QuartzTrigger", b => + { + b.Navigation("BlobTriggers"); + + b.Navigation("CronTriggers"); + + b.Navigation("SimplePropertyTriggers"); + + b.Navigation("SimpleTriggers"); + }); + + modelBuilder.Entity("LexCore.Entities.Project", b => + { + b.Navigation("FlexProjectMetadata"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("LexCore.Entities.User", b => + { + b.Navigation("Projects"); + + b.Navigation("UsersICreated"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.cs b/backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.cs new file mode 100644 index 000000000..556070edb --- /dev/null +++ b/backend/LexData/Migrations/20240425121912_AddIsConfidentialColumnToProjectAndDraftProject.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LexData.Migrations +{ + /// + public partial class AddIsConfidentialColumnToProjectAndDraftProject : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsConfidential", + table: "Projects", + type: "boolean", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsConfidential", + table: "DraftProjects", + type: "boolean", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsConfidential", + table: "Projects"); + + migrationBuilder.DropColumn( + name: "IsConfidential", + table: "DraftProjects"); + } + } +} diff --git a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs index e3db94ea7..f05e25c5f 100644 --- a/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs +++ b/backend/LexData/Migrations/LexBoxDbContextModelSnapshot.cs @@ -485,6 +485,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("IsConfidential") + .HasColumnType("boolean"); + b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -610,6 +613,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("text"); + b.Property("IsConfidential") + .HasColumnType("boolean"); + b.Property("LastCommit") .HasColumnType("timestamp with time zone"); diff --git a/backend/LexData/SeedingData.cs b/backend/LexData/SeedingData.cs index 63dfea5d6..52dd16ee7 100644 --- a/backend/LexData/SeedingData.cs +++ b/backend/LexData/SeedingData.cs @@ -84,6 +84,7 @@ public async Task SeedDatabase(CancellationToken cancellationToken = default) ProjectOrigin = ProjectMigrationStatus.Migrated, LastCommit = DateTimeOffset.UtcNow, RetentionPolicy = RetentionPolicy.Dev, + IsConfidential = null, Users = new() { new() @@ -132,7 +133,8 @@ public async Task SeedDatabase(CancellationToken cancellationToken = default) ProjectOrigin = ProjectMigrationStatus.Migrated, LastCommit = DateTimeOffset.UtcNow, RetentionPolicy = RetentionPolicy.Dev, - Users = [] + IsConfidential = false, + Users = [], }); lexBoxDbContext.Attach(new Organization diff --git a/backend/Testing/LexCore/Services/ProjectServiceTest.cs b/backend/Testing/LexCore/Services/ProjectServiceTest.cs index 66aae2af9..2151e8db8 100644 --- a/backend/Testing/LexCore/Services/ProjectServiceTest.cs +++ b/backend/Testing/LexCore/Services/ProjectServiceTest.cs @@ -32,7 +32,7 @@ public ProjectServiceTest(TestingServicesFixture testing) public async Task CanCreateProject() { var projectId = await _projectService.CreateProject( - new(null, "TestProject", "Test", "test", ProjectType.FLEx, RetentionPolicy.Test, null)); + new(null, "TestProject", "Test", "test", ProjectType.FLEx, RetentionPolicy.Test, false, null)); projectId.ShouldNotBe(default); } @@ -41,10 +41,10 @@ public async Task ShouldErrorIfCreatingAProjectWithTheSameCode() { //first project should be created await _projectService.CreateProject( - new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, null)); + new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, false, null)); var exception = await _projectService.CreateProject( - new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, null) + new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, false, null) ).ShouldThrowAsync(); exception.InnerException.ShouldBeOfType() diff --git a/frontend/schema.graphql b/frontend/schema.graphql index b333b7504..93b8a9056 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -110,6 +110,7 @@ type DraftProject { retentionPolicy: RetentionPolicy! projectManager: User projectManagerId: UUID + isConfidential: Boolean id: UUID! createdDate: DateTime! updatedDate: DateTime! @@ -172,6 +173,7 @@ type Mutation { changeProjectMemberRole(input: ChangeProjectMemberRoleInput!): ChangeProjectMemberRolePayload! changeProjectName(input: ChangeProjectNameInput!): ChangeProjectNamePayload! changeProjectDescription(input: ChangeProjectDescriptionInput!): ChangeProjectDescriptionPayload! + setProjectConfidentiality(input: SetProjectConfidentialityInput!): SetProjectConfidentialityPayload! leaveProject(input: LeaveProjectInput!): LeaveProjectPayload! removeProjectMember(input: RemoveProjectMemberInput!): RemoveProjectMemberPayload! softDeleteProject(input: SoftDeleteProjectInput!): SoftDeleteProjectPayload! @@ -217,6 +219,7 @@ type Project { description: String retentionPolicy: RetentionPolicy! type: ProjectType! + isConfidential: Boolean flexProjectMetadata: FlexProjectMetadata lastCommit: DateTime deletedDate: DateTime @@ -284,6 +287,11 @@ type SetOrgMemberRolePayload { errors: [SetOrgMemberRoleError!] } +type SetProjectConfidentialityPayload { + project: Project + errors: [SetProjectConfidentialityError!] +} + type SetUserLockedPayload { user: User errors: [SetUserLockedError!] @@ -358,6 +366,8 @@ union LeaveProjectError = NotFoundError | LastMemberCantLeaveError union SetOrgMemberRoleError = DbError | NotFoundError +union SetProjectConfidentialityError = NotFoundError | DbError + union SetUserLockedError = NotFoundError union SoftDeleteProjectError = NotFoundError | DbError @@ -421,6 +431,7 @@ input CreateProjectInput { code: String! type: ProjectType! retentionPolicy: RetentionPolicy! + isConfidential: Boolean! projectManagerId: UUID } @@ -453,6 +464,7 @@ input DraftProjectFilterInput { retentionPolicy: RetentionPolicyOperationFilterInput projectManager: UserFilterInput projectManagerId: UuidOperationFilterInput + isConfidential: BooleanOperationFilterInput id: UuidOperationFilterInput createdDate: DateTimeOperationFilterInput updatedDate: DateTimeOperationFilterInput @@ -466,6 +478,7 @@ input DraftProjectSortInput { retentionPolicy: SortEnumType projectManager: UserSortInput projectManagerId: SortEnumType + isConfidential: SortEnumType id: SortEnumType createdDate: SortEnumType updatedDate: SortEnumType @@ -569,6 +582,7 @@ input ProjectFilterInput { description: StringOperationFilterInput retentionPolicy: RetentionPolicyOperationFilterInput type: ProjectTypeOperationFilterInput + isConfidential: BooleanOperationFilterInput flexProjectMetadata: FlexProjectMetadataFilterInput users: ListFilterInputTypeOfProjectUsersFilterInput lastCommit: DateTimeOperationFilterInput @@ -603,6 +617,7 @@ input ProjectSortInput { description: SortEnumType retentionPolicy: SortEnumType type: SortEnumType + isConfidential: SortEnumType flexProjectMetadata: FlexProjectMetadataSortInput lastCommit: SortEnumType deletedDate: SortEnumType @@ -660,6 +675,11 @@ input SetOrgMemberRoleInput { emailOrUsername: String! } +input SetProjectConfidentialityInput { + projectId: UUID! + isConfidential: Boolean! +} + input SetUserLockedInput { userId: UUID! locked: Boolean! diff --git a/frontend/src/lib/app.postcss b/frontend/src/lib/app.postcss index e3ef586cf..bbef11b2a 100644 --- a/frontend/src/lib/app.postcss +++ b/frontend/src/lib/app.postcss @@ -151,8 +151,8 @@ input[readonly]:focus { border-radius: var(--rounded-badge); } -.prose .alert a { - filter: contrast(1.5) +.alert a { + color: #0024b9; } .collapse input:hover ~ .collapse-title { diff --git a/frontend/src/lib/components/Badges/Badge.svelte b/frontend/src/lib/components/Badges/Badge.svelte index f6bb4ec6c..f1543bd7d 100644 --- a/frontend/src/lib/components/Badges/Badge.svelte +++ b/frontend/src/lib/components/Badges/Badge.svelte @@ -8,13 +8,19 @@ export let variant: BadgeVariant = 'badge-neutral'; export let icon: IconString | undefined = undefined; + export let hoverIcon: IconString | undefined = undefined; export let outline = false; - + + + + diff --git a/frontend/src/lib/components/Badges/BadgeButton.svelte b/frontend/src/lib/components/Badges/BadgeButton.svelte index 0189c11b3..a8bf27c42 100644 --- a/frontend/src/lib/components/Badges/BadgeButton.svelte +++ b/frontend/src/lib/components/Badges/BadgeButton.svelte @@ -6,13 +6,12 @@ type BadgeButtonVariant = 'badge-success' | 'badge-warning' | 'badge-neutral' | undefined; export let variant: BadgeButtonVariant = undefined; export let icon: IconString | undefined = undefined; + export let hoverIcon: IconString | undefined = undefined; export let disabled = false; - - $: _type = type as unknown as BadgeVariant; - diff --git a/frontend/src/lib/components/EditableText.svelte b/frontend/src/lib/components/EditableText.svelte index ba45ec60f..9567e551a 100644 --- a/frontend/src/lib/components/EditableText.svelte +++ b/frontend/src/lib/components/EditableText.svelte @@ -125,7 +125,7 @@ {:else} {/if} +

{$t('project_page.summary')}

@@ -345,33 +349,35 @@ {$date(project.lastCommit)}
{#if project.type === ProjectType.FlEx || project.type === ProjectType.WeSay} -
- {$t('project_page.num_entries')}: +
+ {$t('project_page.num_entries')}: + + {$number(lexEntryCount)} + + + + +
+ {/if} +
+
{$t('project_page.description')}:
- {$number(lexEntryCount)} - - - - +
- {/if} -
{$t('project_page.description')}:
- - -
@@ -455,32 +461,39 @@
- - - -

{$t('project_page.leave.confirm_leave')}

-
- {#if canManage} - - + +
+ {#if canManage} + + + {/if} + + +

{$t('project_page.leave.confirm_leave')}

+
+
+ +
+
- -
@@ -495,9 +508,11 @@
- - - {/if} + + +
+ +
diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts b/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts index 91c80b127..a523be2e7 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts +++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.ts @@ -13,6 +13,8 @@ import type { DeleteProjectUserMutation, LeaveProjectMutation, ProjectPageQuery, + SetProjectConfidentialityInput, + SetProjectConfidentialityMutation, } from '$lib/gql/types'; import { getClient, graphql } from '$lib/gql'; @@ -42,6 +44,7 @@ export async function load(event: PageLoadEvent) { lastCommit createdDate retentionPolicy + isConfidential users { id role @@ -89,7 +92,7 @@ export async function load(event: PageLoadEvent) { } `), { projectCode } - ); + ); const nonNullableProject = tryMakeNonNullable(projectResult.projectByCode); if (!nonNullableProject) { @@ -258,6 +261,30 @@ export async function _changeProjectDescription(input: ChangeProjectDescriptionI return result; } +export async function _setProjectConfidentiality(input: SetProjectConfidentialityInput): $OpResult { + //language=GraphQL + const result = await getClient() + .mutation( + graphql(` + mutation SetProjectConfidentiality($input: SetProjectConfidentialityInput!) { + setProjectConfidentiality(input: $input) { + project { + id + isConfidential + } + errors { + ... on Error { + message + } + } + } + } + `), + { input: input } + ); + return result; +} + export async function _deleteProjectUser(projectId: string, userId: string): $OpResult { const result = await getClient() .mutation( @@ -284,7 +311,7 @@ export async function _deleteProjectUser(projectId: string, userId: string): $Op } export async function _refreshProjectRepoInfo(projectCode: string): Promise { - const result = await getClient().query(graphql(` + const result = await getClient().query(graphql(` query refreshProjectStatus($projectCode: String!) { projectByCode(code: $projectCode) { id @@ -309,10 +336,10 @@ export async function _refreshProjectRepoInfo(projectCode: string): Promise { -//language=GraphQL + //language=GraphQL const result = await getClient() - .mutation( - graphql(` + .mutation( + graphql(` mutation LeaveProject($input: LeaveProjectInput!) { leaveProject(input: $input) { project { @@ -324,11 +351,11 @@ export async function _leaveProject(projectId: string): $OpResult

{$t('project_page.open_with_flex.button')}

-
+
@@ -44,8 +44,4 @@ :global(.open-with-flex-modal .collapse-content > *) { margin: 0; } - - :global(.open-with-flex-modal .alert p) { - margin: 0; - } diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityBadge.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityBadge.svelte new file mode 100644 index 000000000..45d4ad9f6 --- /dev/null +++ b/frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityBadge.svelte @@ -0,0 +1,26 @@ + + +{#if isConfidential !== false} + + {#if isConfidential} + {$t('project.confidential.confidential')} + {:else if canManage} + + {$t('project.confidential.set_confidentiality')} + + {:else} + {$t('project.confidential.confidentiality_unspecified')} + {/if} + +{/if} diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityModal.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityModal.svelte new file mode 100644 index 000000000..03e61bd5d --- /dev/null +++ b/frontend/src/routes/(authenticated)/project/[project_code]/ProjectConfidentialityModal.svelte @@ -0,0 +1,51 @@ + + + + {$t('project.confidential.modal.title')} + + + {#if $form.isConfidential} + {$t('project.confidential.modal.submit_button_confidential')} + {:else} + {$t('project.confidential.modal.submit_button_not_confidential')} + {/if} + + diff --git a/frontend/src/routes/(authenticated)/project/create/+page.svelte b/frontend/src/routes/(authenticated)/project/create/+page.svelte index 6ef10eefb..46e60443f 100644 --- a/frontend/src/routes/(authenticated)/project/create/+page.svelte +++ b/frontend/src/routes/(authenticated)/project/create/+page.svelte @@ -15,6 +15,7 @@ import { derived, writable } from 'svelte/store'; import { concatAll } from '$lib/util/array'; import { browser } from '$app/environment'; + import { ProjectConfidentialityCombobox } from '$lib/components/Projects'; export let data; $: user = data.user; @@ -37,6 +38,7 @@ .min(4, $t('project.create.code_too_short')) .regex(/^[a-z\d][a-z-\d]*$/, $t('project.create.code_invalid')), customCode: z.boolean().default(false), + isConfidential: z.boolean().default(false), }); //random guid @@ -49,6 +51,7 @@ description: $form.description, type: $form.type, retentionPolicy: $form.retentionPolicy, + isConfidential: $form.isConfidential, projectManagerId: requestingUser?.id, }); if (result.error) { @@ -109,6 +112,7 @@ if (urlValues.description) form.description = urlValues.description; if (urlValues.type) form.type = urlValues.type; if (urlValues.retentionPolicy && (urlValues.retentionPolicy !== RetentionPolicy.Dev || user.isAdmin)) form.retentionPolicy = urlValues.retentionPolicy; + if (urlValues.isConfidential === 'true') form.isConfidential = true; if (urlValues.code) { const standardCodeSuffix = buildProjectCode('', urlValues.type, urlValues.retentionPolicy); const isCustomCode = !urlValues.code.endsWith(standardCodeSuffix); @@ -199,6 +203,11 @@ error={$errors.description} /> +
+ + +
+ {#if data.user.canCreateProjects} diff --git a/frontend/src/routes/email/tester/+page@.svelte b/frontend/src/routes/email/tester/+page@.svelte index 4ec380c5d..5c8f76d3e 100644 --- a/frontend/src/routes/email/tester/+page@.svelte +++ b/frontend/src/routes/email/tester/+page@.svelte @@ -46,12 +46,13 @@ code: 'myproj-test-onestory', type: ProjectType.OneStoryEditor, description: 'My project description', - retentionPolicy: RetentionPolicy.Test + retentionPolicy: RetentionPolicy.Test, + isConfidential: false, }, user: { name: 'Bob', email: 'test@test.com' - } + }, }, { label: 'Create Project Request (Language Project)', @@ -64,6 +65,7 @@ type: ProjectType.OneStoryEditor, description: 'My project description', retentionPolicy: RetentionPolicy.Verified, + isConfidential: true, }, user: { name: 'Bob', @@ -82,6 +84,7 @@ description: 'My project description', retentionPolicy: RetentionPolicy.Dev, projectManagerId: '703701a8-005c-4747-91f2-ac7650455118', // manager from seeding data + isConfidential: true, }, user: { name: 'Bob', diff --git a/frontend/tests/fixtures.ts b/frontend/tests/fixtures.ts index bfbe16f36..390ed5e9f 100644 --- a/frontend/tests/fixtures.ts +++ b/frontend/tests/fixtures.ts @@ -97,7 +97,8 @@ export const test = base.extend({ type: FL_EX, code: "${code}", description: "temporary project created during the ${testInfo.title} unit test", - retentionPolicy: DEV + retentionPolicy: DEV, + isConfidential: false }) { createProjectResponse { id