From 6bb4741a05c198ac48e8b496b201624ce99cedbe Mon Sep 17 00:00:00 2001 From: Mathieu Guindon Date: Tue, 10 Jun 2025 20:38:40 -0400 Subject: [PATCH 1/4] add admin endpoints, edit summary/description --- .../Api/Admin/AdminController.cs | 5 +- .../Api/Features/FeatureEditViewModel.cs | 8 +- .../Api/Features/FeatureViewModel.cs | 2 +- .../Api/Features/FeaturesController.cs | 44 +++++---- .../Model/Entity/FeatureEntity.cs | 2 +- rubberduckvba.Server/Model/Feature.cs | 6 +- rubberduckvba.Server/Program.cs | 13 +-- .../Services/RubberduckDbService.cs | 2 +- .../Services/rubberduckdb/FeatureServices.cs | 2 +- rubberduckvba.client/src/app/app.module.ts | 5 +- .../edit-feature/edit-feature.component.html | 85 +++++++++++++++++ .../edit-feature/edit-feature.component.ts | 91 +++++++++++++++++++ .../feature-box/feature-box.component.html | 9 ++ .../feature-box/feature-box.component.ts | 53 ++++++++--- .../feature-info/feature-info.component.html | 4 + .../feature-info/feature-info.component.ts | 26 ++++-- .../src/app/model/feature.model.ts | 16 ++++ .../routes/features/features.component.html | 2 +- .../app/routes/features/features.component.ts | 13 +-- .../src/app/services/api-client.service.ts | 20 +++- .../src/app/services/data.service.ts | 10 +- .../src/environments/environment.prod.ts | 2 +- .../src/environments/environment.ts | 2 +- 23 files changed, 333 insertions(+), 89 deletions(-) create mode 100644 rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html create mode 100644 rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts diff --git a/rubberduckvba.Server/Api/Admin/AdminController.cs b/rubberduckvba.Server/Api/Admin/AdminController.cs index c478ca3..382b1ee 100644 --- a/rubberduckvba.Server/Api/Admin/AdminController.cs +++ b/rubberduckvba.Server/Api/Admin/AdminController.cs @@ -7,6 +7,7 @@ namespace rubberduckvba.Server.Api.Admin; [ApiController] +[EnableCors(CorsPolicies.AllowAll)] public class AdminController(ConfigurationOptions options, HangfireLauncherService hangfire, CacheService cache) : ControllerBase { /// @@ -14,7 +15,6 @@ public class AdminController(ConfigurationOptions options, HangfireLauncherServi /// /// The unique identifier of the enqueued job. [Authorize("github")] - [EnableCors(CorsPolicies.AllowAuthenticated)] [HttpPost("admin/update/xmldoc")] public IActionResult UpdateXmldocContent() { @@ -27,7 +27,6 @@ public IActionResult UpdateXmldocContent() /// /// The unique identifier of the enqueued job. [Authorize("github")] - [EnableCors(CorsPolicies.AllowAuthenticated)] [HttpPost("admin/update/tags")] public IActionResult UpdateTagMetadata() { @@ -36,7 +35,6 @@ public IActionResult UpdateTagMetadata() } [Authorize("github")] - [EnableCors(CorsPolicies.AllowAuthenticated)] [HttpPost("admin/cache/clear")] public IActionResult ClearCache() { @@ -46,7 +44,6 @@ public IActionResult ClearCache() #if DEBUG [AllowAnonymous] - [EnableCors(CorsPolicies.AllowAll)] [HttpGet("admin/config/current")] public IActionResult Config() { diff --git a/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs b/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs index 62868d6..fa3d9f6 100644 --- a/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs +++ b/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs @@ -17,7 +17,7 @@ public static FeatureEditViewModel Default(RepositoryId repository, FeatureOptio Repositories = repositories, RepositoryId = repository, - ParentId = parent?.Id, + FeatureId = parent?.Id, Name = parent is null ? "NewFeature" : $"New{parent.Name}Feature", Title = "Feature Title", ShortDescription = "A short description; markdown is supported.", @@ -29,7 +29,7 @@ public Feature ToFeature() return new Feature { Id = Id ?? default, - FeatureId = ParentId, + ParentId = FeatureId, RepositoryId = RepositoryId, Name = Name, Title = Title, @@ -43,7 +43,7 @@ public Feature ToFeature() public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, RepositoryOptionViewModel[] repositories) { Id = model.Id; - ParentId = model.FeatureId; + FeatureId = model.ParentId; RepositoryId = model.RepositoryId; Name = model.Name; @@ -59,7 +59,7 @@ public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, Re } public int? Id { get; init; } - public int? ParentId { get; init; } + public int? FeatureId { get; init; } public RepositoryId RepositoryId { get; init; } public string Name { get; init; } diff --git a/rubberduckvba.Server/Api/Features/FeatureViewModel.cs b/rubberduckvba.Server/Api/Features/FeatureViewModel.cs index a47439d..062734c 100644 --- a/rubberduckvba.Server/Api/Features/FeatureViewModel.cs +++ b/rubberduckvba.Server/Api/Features/FeatureViewModel.cs @@ -11,7 +11,7 @@ public FeatureViewModel(Feature model, bool summaryOnly = false) DateInserted = model.DateTimeInserted; DateUpdated = model.DateTimeUpdated; - FeatureId = model.FeatureId; + FeatureId = model.ParentId; FeatureName = model.FeatureName; Name = model.Name; diff --git a/rubberduckvba.Server/Api/Features/FeaturesController.cs b/rubberduckvba.Server/Api/Features/FeaturesController.cs index 6868419..df32afc 100644 --- a/rubberduckvba.Server/Api/Features/FeaturesController.cs +++ b/rubberduckvba.Server/Api/Features/FeaturesController.cs @@ -9,8 +9,14 @@ namespace rubberduckvba.Server.Api.Features; +public class MarkdownContentViewModel +{ + public string Content { get; init; } = string.Empty; +} + [AllowAnonymous] +[EnableCors(CorsPolicies.AllowAll)] public class FeaturesController : RubberduckApiController { private readonly CacheService cache; @@ -18,8 +24,9 @@ public class FeaturesController : RubberduckApiController private readonly FeatureServices features; private readonly IRepository assetsRepository; private readonly IRepository tagsRepository; + private readonly IMarkdownFormattingService markdownService; - public FeaturesController(CacheService cache, IRubberduckDbService db, FeatureServices features, + public FeaturesController(CacheService cache, IRubberduckDbService db, FeatureServices features, IMarkdownFormattingService markdownService, IRepository assetsRepository, IRepository tagsRepository, ILogger logger) : base(logger) { @@ -28,6 +35,7 @@ public FeaturesController(CacheService cache, IRubberduckDbService db, FeatureSe this.features = features; this.assetsRepository = assetsRepository; this.tagsRepository = tagsRepository; + this.markdownService = markdownService; } private static RepositoryOptionViewModel[] RepositoryOptions { get; } = @@ -38,8 +46,6 @@ await db.GetTopLevelFeatures(repositoryId) .ContinueWith(t => t.Result.Select(e => new FeatureOptionViewModel { Id = e.Id, Name = e.Name, Title = e.Title }).ToArray()); [HttpGet("features")] - [EnableCors(CorsPolicies.AllowAll)] - [AllowAnonymous] public IActionResult Index() { return GuardInternalAction(() => @@ -68,8 +74,6 @@ public IActionResult Index() } [HttpGet("features/{name}")] - [EnableCors(CorsPolicies.AllowAll)] - [AllowAnonymous] public IActionResult Info([FromRoute] string name) { return GuardInternalAction(() => @@ -85,8 +89,6 @@ public IActionResult Info([FromRoute] string name) } [HttpGet("inspections/{name}")] - [EnableCors(CorsPolicies.AllowAll)] - [AllowAnonymous] public IActionResult Inspection([FromRoute] string name) { return GuardInternalAction(() => @@ -107,8 +109,6 @@ public IActionResult Inspection([FromRoute] string name) } [HttpGet("annotations/{name}")] - [EnableCors(CorsPolicies.AllowAll)] - [AllowAnonymous] public IActionResult Annotation([FromRoute] string name) { return GuardInternalAction(() => @@ -129,8 +129,6 @@ public IActionResult Annotation([FromRoute] string name) } [HttpGet("quickfixes/{name}")] - [EnableCors(CorsPolicies.AllowAll)] - [AllowAnonymous] public IActionResult QuickFix([FromRoute] string name) { return GuardInternalAction(() => @@ -151,7 +149,6 @@ public IActionResult QuickFix([FromRoute] string name) } [HttpGet("features/create")] - [EnableCors(CorsPolicies.AllowAuthenticated)] [Authorize("github")] public async Task> Create([FromQuery] RepositoryId repository = RepositoryId.Rubberduck, [FromQuery] int? parentId = default) { @@ -163,7 +160,6 @@ public async Task> Create([FromQuery] Reposit } [HttpPost("create")] - [EnableCors(CorsPolicies.AllowAuthenticated)] [Authorize("github")] public async Task> Create([FromBody] FeatureEditViewModel model) { @@ -179,14 +175,14 @@ public async Task> Create([FromBody] FeatureE } var feature = model.ToFeature(); - var result = await db.SaveFeature(feature); + var result = await db.SaveFeature(feature); var features = await GetFeatureOptions(model.RepositoryId); + return Ok(new FeatureEditViewModel(result, features, RepositoryOptions)); } [HttpPost("features/update")] - [EnableCors(CorsPolicies.AllowAuthenticated)] [Authorize("github")] public async Task> Update([FromBody] FeatureEditViewModel model) { @@ -201,12 +197,25 @@ public async Task> Update([FromBody] FeatureE return BadRequest("Model is invalid for this endpoint."); } - var result = await db.SaveFeature(model.ToFeature()); + var feature = model.ToFeature(); + + var result = await db.SaveFeature(feature); var features = await GetFeatureOptions(model.RepositoryId); return new FeatureEditViewModel(result, features, RepositoryOptions); } + [HttpPost("markdown/format")] + public MarkdownContentViewModel FormatMarkdown([FromBody] MarkdownContentViewModel model) + { + var markdown = model.Content; + var formatted = markdownService.FormatMarkdownDocument(markdown, withSyntaxHighlighting: true); + return new MarkdownContentViewModel + { + Content = formatted + }; + } + private InspectionsFeatureViewModel GetInspections() { InspectionsFeatureViewModel result; @@ -274,5 +283,4 @@ private FeatureViewModel GetFeature(string name) return result; } - -} +} \ No newline at end of file diff --git a/rubberduckvba.Server/Model/Entity/FeatureEntity.cs b/rubberduckvba.Server/Model/Entity/FeatureEntity.cs index 38b2803..a623791 100644 --- a/rubberduckvba.Server/Model/Entity/FeatureEntity.cs +++ b/rubberduckvba.Server/Model/Entity/FeatureEntity.cs @@ -4,7 +4,7 @@ namespace rubberduckvba.Server.Model.Entity; public record class FeatureEntity : Entity { - public int? FeatureId { get; init; } + public int? ParentId { get; init; } public string FeatureName { get; init; } = default!; public int RepositoryId { get; init; } public string Title { get; init; } = default!; diff --git a/rubberduckvba.Server/Model/Feature.cs b/rubberduckvba.Server/Model/Feature.cs index b7d0b30..9926c0c 100644 --- a/rubberduckvba.Server/Model/Feature.cs +++ b/rubberduckvba.Server/Model/Feature.cs @@ -27,7 +27,7 @@ public Feature(FeatureEntity entity) : this() DateTimeInserted = entity.DateTimeInserted; DateTimeUpdated = entity.DateTimeUpdated; Name = entity.Name; - FeatureId = entity.FeatureId; + ParentId = entity.ParentId; FeatureName = entity.FeatureName; RepositoryId = (Services.RepositoryId)entity.RepositoryId; Title = entity.Title; @@ -45,7 +45,7 @@ public Feature(FeatureEntity entity) : this() public DateTime? DateTimeUpdated { get; init; } public string Name { get; init; } = string.Empty; - public int? FeatureId { get; init; } + public int? ParentId { get; init; } public string FeatureName { get; init; } = string.Empty; public Services.RepositoryId RepositoryId { get; init; } = Services.RepositoryId.Rubberduck; public string Title { get; init; } = string.Empty; @@ -69,7 +69,7 @@ public Feature(FeatureEntity entity) : this() IsNew = IsNew, Name = Name, ShortDescription = ShortDescription, - FeatureId = FeatureId, + ParentId = ParentId, FeatureName = FeatureName, RepositoryId = (int)Services.RepositoryId.Rubberduck, Title = Title, diff --git a/rubberduckvba.Server/Program.cs b/rubberduckvba.Server/Program.cs index 05feb62..216e39c 100644 --- a/rubberduckvba.Server/Program.cs +++ b/rubberduckvba.Server/Program.cs @@ -35,7 +35,6 @@ public class HangfireAuthenticationFilter : IDashboardAuthorizationFilter public static class CorsPolicies { public const string AllowAll = "AllowAll"; - public const string AllowAuthenticated = "AllowAuthenticated"; } public class Program @@ -57,17 +56,9 @@ public static void Main(string[] args) { policy .SetIsOriginAllowed(origin => true) - .AllowAnyHeader() - .WithMethods("OPTIONS", "GET", "POST") - .Build(); - }); - builder.AddPolicy(CorsPolicies.AllowAuthenticated, policy => - { - policy - .SetIsOriginAllowed(origin => true) - .WithHeaders("X-ACCESS-TOKEN") .WithMethods("OPTIONS", "GET", "POST") - .AllowCredentials() + .AllowAnyHeader() + .AllowAnyOrigin() .Build(); }); }); diff --git a/rubberduckvba.Server/Services/RubberduckDbService.cs b/rubberduckvba.Server/Services/RubberduckDbService.cs index 95a88ad..2a17a67 100644 --- a/rubberduckvba.Server/Services/RubberduckDbService.cs +++ b/rubberduckvba.Server/Services/RubberduckDbService.cs @@ -160,7 +160,7 @@ public async Task ResolveFeature(RepositoryId repositoryId, string { var features = _featureServices.Get(topLevelOnly: false).ToList(); var feature = features.Single(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase)); - var children = features.Where(e => e.FeatureId == feature.Id); + var children = features.Where(e => e.ParentId == feature.Id); return new FeatureGraph(feature.ToEntity()) { Features = children.ToArray() diff --git a/rubberduckvba.Server/Services/rubberduckdb/FeatureServices.cs b/rubberduckvba.Server/Services/rubberduckdb/FeatureServices.cs index 2a0033f..3f9149f 100644 --- a/rubberduckvba.Server/Services/rubberduckdb/FeatureServices.cs +++ b/rubberduckvba.Server/Services/rubberduckdb/FeatureServices.cs @@ -15,7 +15,7 @@ public class FeatureServices( public IEnumerable Get(bool topLevelOnly = true) { return featureRepository.GetAll() - .Where(e => !topLevelOnly || e.FeatureId is null) + .Where(e => !topLevelOnly || e.ParentId is null) .Select(e => new Feature(e)); } diff --git a/rubberduckvba.client/src/app/app.module.ts b/rubberduckvba.client/src/app/app.module.ts index 86f5ce9..6cf1f25 100644 --- a/rubberduckvba.client/src/app/app.module.ts +++ b/rubberduckvba.client/src/app/app.module.ts @@ -26,6 +26,8 @@ import { AnnotationItemBoxComponent } from './components/feature-item-box/annota import { BlogLinkBoxComponent } from './components/blog-link-box/blog-link-box.component'; import { QuickFixItemBoxComponent } from './components/feature-item-box/quickfix-item-box/quickfix-item-box.component'; +import { EditFeatureComponent } from './components/edit-feature/edit-feature.component'; + import { HomeComponent } from './routes/home/home.component'; import { AboutComponent } from './routes/about/about.component'; import { FeaturesComponent } from './routes/features/features.component'; @@ -78,7 +80,8 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { InspectionComponent, AnnotationComponent, QuickFixComponent, - AboutComponent + AboutComponent, + EditFeatureComponent ], bootstrap: [AppComponent], imports: [ diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html new file mode 100644 index 0000000..dd788c2 --- /dev/null +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html @@ -0,0 +1,85 @@ + + + + + + + + + +
+ + + +
+
+ + +
+ + + +
+
diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts new file mode 100644 index 0000000..621c69d --- /dev/null +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts @@ -0,0 +1,91 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, TemplateRef, ViewChild, inject, input } from "@angular/core"; +import { BehaviorSubject } from "rxjs"; +import { EditSubFeatureViewModelClass, MarkdownContent, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel } from "../../model/feature.model"; +import { FaIconLibrary } from "@fortawesome/angular-fontawesome"; +import { fas } from "@fortawesome/free-solid-svg-icons"; +import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; +import { ApiClientService } from "../../services/api-client.service"; + +export enum AdminAction { + Edit = 'edit', + EditSummary = 'summary', + Create = 'create', + Delete = 'delete', +} + +@Component({ + selector: 'edit-feature', + templateUrl: './edit-feature.component.html' +}) +export class EditFeatureComponent implements OnInit, OnChanges { + private readonly _user: BehaviorSubject = new BehaviorSubject(null!); + private readonly _feature: BehaviorSubject = new BehaviorSubject(null!); + + private _action: AdminAction = AdminAction.Create; + + @ViewChild('editModal', { read: TemplateRef }) editModal: TemplateRef | undefined; + @ViewChild('deleteModal', { read: TemplateRef }) deleteModal: TemplateRef | undefined; + + public modal = inject(NgbModal); + + @Input() + public set feature(value: SubFeatureViewModel | undefined) { + if (value != null) { + this._feature.next(new EditSubFeatureViewModelClass(value)); + } + } + + public get feature(): EditSubFeatureViewModelClass { + return this._feature.value; + } + + @Input() + public set action(value: AdminAction) { + this._action = value; + } + + public get action(): AdminAction { + return this._action; + } + + @Output() + public onApplyChanges = new EventEmitter(); + + + constructor(private fa: FaIconLibrary, private api: ApiClientService) { + fa.addIconPacks(fas); + } + + ngOnChanges(changes: SimpleChanges): void { + } + + ngOnInit(): void { + } + + public doAction(): void { + const localModal = this.action == 'delete' ? this.deleteModal : this.editModal; + const size = this.action == 'delete' ? 'modal-m' : 'modal-xl'; + this.modal.open(localModal, { modalDialogClass: size }); + } + + public onConfirmChanges(): void { + this.modal.dismissAll(); + this.api.saveFeature(this.feature).subscribe(saved => { + this._feature.next(new EditSubFeatureViewModelClass(saved)); + this.onApplyChanges.emit(saved) + }); + } + + public onPreviewDescription(): void { + this.api.formatMarkdown(this.feature.description).subscribe((formatted: MarkdownContent) => { + this.feature.descriptionPreview = formatted.content; + }); + } + + public onDeleteFeature(): void { + this.modal.dismissAll(); + this.api.deleteFeature(this.feature).subscribe(() => { + this._feature.next(new EditSubFeatureViewModelClass(null!)); + }); + } +} diff --git a/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html b/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html index a63f3e1..908b67c 100644 --- a/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html +++ b/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html @@ -29,5 +29,14 @@

{{feature.title}}

+ diff --git a/rubberduckvba.client/src/app/components/feature-box/feature-box.component.ts b/rubberduckvba.client/src/app/components/feature-box/feature-box.component.ts index 7a6eb20..0b5d70d 100644 --- a/rubberduckvba.client/src/app/components/feature-box/feature-box.component.ts +++ b/rubberduckvba.client/src/app/components/feature-box/feature-box.component.ts @@ -1,17 +1,20 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges, TemplateRef, ViewChild, inject } from '@angular/core'; -import { FeatureViewModel, QuickFixViewModel, SubFeatureViewModel } from '../../model/feature.model'; +import { FeatureViewModel, QuickFixViewModel, SubFeatureViewModel, UserViewModel } from '../../model/feature.model'; import { BehaviorSubject } from 'rxjs'; import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { fas } from '@fortawesome/free-solid-svg-icons'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AuthService } from '../../services/auth.service'; +import { AdminAction } from '../edit-feature/edit-feature.component'; @Component({ selector: 'feature-box', templateUrl: './feature-box.component.html' }) -export class FeatureBoxComponent implements OnInit { +export class FeatureBoxComponent implements OnInit, OnChanges { private readonly _info: BehaviorSubject = new BehaviorSubject(null!); + private readonly _user: BehaviorSubject = new BehaviorSubject(null!); @ViewChild('content', { read: TemplateRef }) content: TemplateRef | undefined; public modal = inject(NgbModal); @@ -22,6 +25,20 @@ export class FeatureBoxComponent implements OnInit { @Input() public hasOwnDetailsPage: boolean = false; + public get isProtected(): boolean { + return this.feature?.name == 'Inspections' + || this.feature?.name == 'QuickFixes' + || this.feature?.name == 'Annotations' + || this.feature?.name == 'CodeInspections' + || this.feature?.name == 'CommentAnnotations'; + } + + public get hasXmlDocFeatures(): boolean { + return this.feature?.name == 'Inspections' + || this.feature?.name == 'QuickFixes' + || this.feature?.name == 'Annotations'; + } + @Input() public set feature(value: FeatureViewModel | undefined) { if (value != null) { @@ -29,6 +46,11 @@ export class FeatureBoxComponent implements OnInit { } } + public editAction: AdminAction = AdminAction.EditSummary; + public editDetailsAction: AdminAction = AdminAction.Edit; + public createAction: AdminAction = AdminAction.Create; + public deleteAction: AdminAction = AdminAction.Delete; + public get feature(): FeatureViewModel | undefined { return this._info.value as FeatureViewModel; } @@ -37,27 +59,28 @@ export class FeatureBoxComponent implements OnInit { return this._info.value as SubFeatureViewModel; } - private readonly _quickfixes: BehaviorSubject = new BehaviorSubject(null!); - - @Input() - public set quickFixes(value: QuickFixViewModel[]) { - if (value != null) { - this._quickfixes.next(value); - } + constructor(private fa: FaIconLibrary, private auth: AuthService) { + fa.addIconPacks(fas); } - - public get quickFixes(): QuickFixViewModel[] { - return this._quickfixes.value; + ngOnChanges(changes: SimpleChanges): void { + console.log(changes); } - constructor(private fa: FaIconLibrary) { - fa.addIconPacks(fas); + ngOnInit(): void { + this.auth.getUser().subscribe(vm => { + this._user.next(vm); + }); } - ngOnInit(): void { + public applyChanges(model: any): void { + this._info.next(model); } public showDetails(): void { this.modal.open(this.content); } + + public get user(): UserViewModel { + return this._user.getValue(); + } } diff --git a/rubberduckvba.client/src/app/components/feature-info/feature-info.component.html b/rubberduckvba.client/src/app/components/feature-info/feature-info.component.html index 7e4620a..2453a08 100644 --- a/rubberduckvba.client/src/app/components/feature-info/feature-info.component.html +++ b/rubberduckvba.client/src/app/components/feature-info/feature-info.component.html @@ -4,6 +4,9 @@

HomeFeatures{{feature?.featureName}}

{{feature?.title}}

+
+ +

@@ -34,6 +37,7 @@

{{feature?.title}}

+
diff --git a/rubberduckvba.client/src/app/components/feature-info/feature-info.component.ts b/rubberduckvba.client/src/app/components/feature-info/feature-info.component.ts index 1c51009..19de4d5 100644 --- a/rubberduckvba.client/src/app/components/feature-info/feature-info.component.ts +++ b/rubberduckvba.client/src/app/components/feature-info/feature-info.component.ts @@ -1,17 +1,24 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { AnnotationViewModel, AnnotationsFeatureViewModel, BlogLink, FeatureViewModel, InspectionViewModel, InspectionsFeatureViewModel, QuickFixViewModel, QuickFixesFeatureViewModel, SubFeatureViewModel, XmlDocItemViewModel, XmlDocOrFeatureViewModel, XmlDocViewModel } from '../../model/feature.model'; +import { AnnotationViewModel, AnnotationsFeatureViewModel, BlogLink, FeatureViewModel, InspectionViewModel, InspectionsFeatureViewModel, QuickFixViewModel, QuickFixesFeatureViewModel, SubFeatureViewModel, UserViewModel, XmlDocItemViewModel, XmlDocOrFeatureViewModel, XmlDocViewModel } from '../../model/feature.model'; import { BehaviorSubject } from 'rxjs'; import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { fas } from '@fortawesome/free-solid-svg-icons'; import { ApiClientService } from '../../services/api-client.service'; +import { AuthService } from '../../services/auth.service'; +import { AdminAction } from '../edit-feature/edit-feature.component'; @Component({ selector: 'feature-info', templateUrl: './feature-info.component.html', }) -export class FeatureInfoComponent implements OnInit, OnChanges { +export class FeatureInfoComponent implements OnInit { private readonly _info: BehaviorSubject = new BehaviorSubject(null!); + private readonly _user: BehaviorSubject = new BehaviorSubject(null!); + + public editAction: AdminAction = AdminAction.Edit; + public createAction: AdminAction = AdminAction.Create; + public deleteAction: AdminAction = AdminAction.Delete; public filterState = { // searchbox @@ -68,11 +75,20 @@ export class FeatureInfoComponent implements OnInit, OnChanges { return feature?.links ?? []; } - constructor(private api: ApiClientService, private fa: FaIconLibrary) { + public get user(): UserViewModel { + return this._user.getValue(); + } + + constructor(private auth: AuthService, private api: ApiClientService, private fa: FaIconLibrary) { fa.addIconPacks(fas); } ngOnInit(): void { + this.auth.getUser().subscribe(result => { + if (result) { + this._user.next(result as UserViewModel); + } + }); this.api.getFeature('quickfixes').subscribe(result => { if (result) { this._quickfixes.next((result as QuickFixesFeatureViewModel).quickFixes.slice()); @@ -80,10 +96,6 @@ export class FeatureInfoComponent implements OnInit, OnChanges { }); } - - ngOnChanges(changes: SimpleChanges): void { - } - public onFilter(): void { this.filterByNameOrDescription(this.filterState.filterText); } diff --git a/rubberduckvba.client/src/app/model/feature.model.ts b/rubberduckvba.client/src/app/model/feature.model.ts index 97a0534..f20df66 100644 --- a/rubberduckvba.client/src/app/model/feature.model.ts +++ b/rubberduckvba.client/src/app/model/feature.model.ts @@ -10,6 +10,10 @@ export interface ViewModel { isDetailsCollapsed: boolean; } +export interface MarkdownContent { + content: string; +} + export interface SubFeatureViewModel extends ViewModel { featureId?: number; featureName?: string; @@ -272,6 +276,18 @@ export class SubFeatureViewModelClass extends ViewModelBase implements SubFeatur } } +export class EditSubFeatureViewModelClass extends SubFeatureViewModelClass { + constructor(model: SubFeatureViewModel) { + super(model); + this.isDetailsCollapsed = false; + this.descriptionPreview = model.description; + this.shortDescription = (model as FeatureViewModel)?.shortDescription; + } + + public descriptionPreview: string; + public shortDescription?: string; +} + export class InspectionViewModelClass extends SubFeatureViewModelClass implements InspectionViewModel { inspectionType: string; defaultSeverity: string; diff --git a/rubberduckvba.client/src/app/routes/features/features.component.html b/rubberduckvba.client/src/app/routes/features/features.component.html index 2366bed..f55801a 100644 --- a/rubberduckvba.client/src/app/routes/features/features.component.html +++ b/rubberduckvba.client/src/app/routes/features/features.component.html @@ -25,7 +25,7 @@

Your IDE is incomplete without...

diff --git a/rubberduckvba.client/src/app/routes/features/features.component.ts b/rubberduckvba.client/src/app/routes/features/features.component.ts index cf7c8e6..5e1acd7 100644 --- a/rubberduckvba.client/src/app/routes/features/features.component.ts +++ b/rubberduckvba.client/src/app/routes/features/features.component.ts @@ -1,4 +1,4 @@ -import { Component, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { ApiClientService } from "../../services/api-client.service"; import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { fas } from '@fortawesome/free-solid-svg-icons'; @@ -9,7 +9,7 @@ import { FeatureViewModel, QuickFixViewModel } from '../../model/feature.model'; selector: 'app-features', templateUrl: './features.component.html', }) -export class FeaturesComponent implements OnInit, OnChanges { +export class FeaturesComponent implements OnInit { private readonly _features: BehaviorSubject = new BehaviorSubject(null!); public set features(value: FeatureViewModel[]) { @@ -19,19 +19,10 @@ export class FeaturesComponent implements OnInit, OnChanges { return this._features.getValue(); } - private readonly _quickFixes: BehaviorSubject = new BehaviorSubject(null!); - public get quickFixes(): QuickFixViewModel[] { - return this._quickFixes.value; - } - constructor(private api: ApiClientService, private fa: FaIconLibrary) { fa.addIconPacks(fas); } - ngOnChanges(changes: SimpleChanges): void { - console.log(changes); - } - ngOnInit(): void { this.api.getFeatureSummaries().subscribe(result => { if (result) { diff --git a/rubberduckvba.client/src/app/services/api-client.service.ts b/rubberduckvba.client/src/app/services/api-client.service.ts index 5be402b..37d4899 100644 --- a/rubberduckvba.client/src/app/services/api-client.service.ts +++ b/rubberduckvba.client/src/app/services/api-client.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { LatestTags, Tag } from "../model/tags.model"; -import { AnnotationViewModel, AnnotationViewModelClass, AnnotationsFeatureViewModel, AnnotationsFeatureViewModelClass, FeatureViewModel, FeatureViewModelClass, InspectionViewModel, InspectionViewModelClass, InspectionsFeatureViewModel, InspectionsFeatureViewModelClass, QuickFixViewModel, QuickFixViewModelClass, QuickFixesFeatureViewModel, QuickFixesFeatureViewModelClass, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel, XmlDocOrFeatureViewModel } from "../model/feature.model"; +import { AnnotationViewModel, AnnotationViewModelClass, AnnotationsFeatureViewModel, AnnotationsFeatureViewModelClass, FeatureViewModel, FeatureViewModelClass, InspectionViewModel, InspectionViewModelClass, InspectionsFeatureViewModel, InspectionsFeatureViewModelClass, MarkdownContent, QuickFixViewModel, QuickFixViewModelClass, QuickFixesFeatureViewModel, QuickFixesFeatureViewModelClass, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel, XmlDocOrFeatureViewModel } from "../model/feature.model"; import { DownloadInfo } from "../model/downloads.model"; import { DataService } from "./data.service"; import { environment } from "../../environments/environment.prod"; @@ -82,4 +82,22 @@ export class ApiClientService { return model; })); } + + public saveFeature(model: SubFeatureViewModel): Observable { + const url = `${environment.apiBaseUrl}features/update`; + return this.data.postAsync(url, model).pipe(map(result => new SubFeatureViewModelClass(result as SubFeatureViewModel))); + } + + public deleteFeature(model: SubFeatureViewModel): Observable { + const url = `${environment.apiBaseUrl}features/delete`; + return this.data.postAsync(url, model).pipe(map(() => model)); + } + + public formatMarkdown(raw: string): Observable { + const url = `${environment.apiBaseUrl}markdown/format`; + const content: MarkdownContent = { + content: raw + }; + return this.data.postAsync(url, content); + } } diff --git a/rubberduckvba.client/src/app/services/data.service.ts b/rubberduckvba.client/src/app/services/data.service.ts index c66a376..748b7ea 100644 --- a/rubberduckvba.client/src/app/services/data.service.ts +++ b/rubberduckvba.client/src/app/services/data.service.ts @@ -14,13 +14,11 @@ export class DataService { let headers = new HttpHeaders() .append('accept', 'application/json'); const token = sessionStorage.getItem('github:access_token'); - let withCreds = false; if (token) { headers = headers.append('X-ACCESS-TOKEN', token); - withCreds = true; } - return this.http.get(url, { headers, withCredentials: withCreds }) + return this.http.get(url, { headers }) .pipe( map(result => result), timeout(this.timeout), @@ -38,15 +36,13 @@ export class DataService { .append('Content-Type', 'application/json; charset=utf-8'); const token = sessionStorage.getItem('github:access_token'); - let withCreds = false; if (token) { headers = headers.append('X-ACCESS-TOKEN', token); - withCreds = true; } return (content - ? this.http.post(url, content, { headers, withCredentials: withCreds }) - : this.http.post(url, null, { headers, withCredentials: withCreds })) + ? this.http.post(url, content, { headers } ) + : this.http.post(url, null, { headers })) .pipe( map(result => result), timeout(this.timeout), diff --git a/rubberduckvba.client/src/environments/environment.prod.ts b/rubberduckvba.client/src/environments/environment.prod.ts index d3de473..e95e3c7 100644 --- a/rubberduckvba.client/src/environments/environment.prod.ts +++ b/rubberduckvba.client/src/environments/environment.prod.ts @@ -1,4 +1,4 @@ export const environment = { production: true, - apiBaseUrl: 'https://api.rubberduckvba.com/' + apiBaseUrl: 'https://localhost:44314/' }; diff --git a/rubberduckvba.client/src/environments/environment.ts b/rubberduckvba.client/src/environments/environment.ts index fbede92..0fd806b 100644 --- a/rubberduckvba.client/src/environments/environment.ts +++ b/rubberduckvba.client/src/environments/environment.ts @@ -4,7 +4,7 @@ export const environment = { production: false, - apiBaseUrl: 'https://api.rubberduckvba.com/' + apiBaseUrl: 'https://localhost:44314/' }; /* From 5d62997a0e943c4099e43c7b1d6ebca01ebe59ef Mon Sep 17 00:00:00 2001 From: Mathieu Guindon Date: Tue, 10 Jun 2025 22:31:40 -0400 Subject: [PATCH 2/4] add create mechanics --- .../edit-feature/edit-feature.component.html | 84 ++++++++++++++++++- .../edit-feature/edit-feature.component.ts | 54 ++++++++++-- .../src/app/model/feature.model.ts | 5 +- 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html index dd788c2..12ca603 100644 --- a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html @@ -35,7 +35,7 @@
Edit {{action == 'edit' ? 'description' : 'summary'}}
-
@@ -64,6 +64,88 @@

{{feature.title}}

+ +
+ + + +
+
+
{{feature.description.length}} - {{feature.shortDescription?.length ?? 0}} -
+ {{feature.shortDescription.length}} +
x