Skip to content

Commit 3e4f0e3

Browse files
authored
Merge pull request #78 from rubberduck-vba/webhook
More admin/reviewer features
2 parents d711039 + 5e59420 commit 3e4f0e3

18 files changed

+209
-63
lines changed

rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using rubberduckvba.Server.Model;
2+
using rubberduckvba.Server.Model.Entity;
23
using rubberduckvba.Server.Services;
34

45
namespace rubberduckvba.Server.Api.Features;
@@ -36,7 +37,8 @@ public Feature ToFeature()
3637
ShortDescription = ShortDescription,
3738
Description = Description,
3839
IsHidden = IsHidden,
39-
IsNew = IsNew
40+
IsNew = IsNew,
41+
Links = Links
4042
};
4143
}
4244

@@ -56,6 +58,8 @@ public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, Re
5658

5759
Features = features;
5860
Repositories = repositories;
61+
62+
Links = model.Links;
5963
}
6064

6165
public int? Id { get; init; }
@@ -72,4 +76,6 @@ public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, Re
7276

7377
public FeatureOptionViewModel[] Features { get; init; } = [];
7478
public RepositoryOptionViewModel[] Repositories { get; init; } = [];
79+
80+
public BlogLink[] Links { get; init; } = [];
7581
}

rubberduckvba.Server/Api/Features/FeaturesController.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,13 @@ public async Task Delete([FromBody] Feature model)
244244
public MarkdownContentViewModel FormatMarkdown([FromBody] MarkdownContentViewModel model)
245245
{
246246
var markdown = model.Content;
247-
var formatted = markdownService.FormatMarkdownDocument(markdown, withSyntaxHighlighting: true);
248-
return new MarkdownContentViewModel
247+
if (!cache.TryGetHtml(markdown, out var html))
249248
{
250-
Content = formatted
251-
};
249+
html = markdownService.FormatMarkdownDocument(markdown, withSyntaxHighlighting: true);
250+
cache.CacheHtml(markdown, html);
251+
}
252+
253+
return new MarkdownContentViewModel { Content = html! };
252254
}
253255

254256
private InspectionsFeatureViewModel GetInspections()

rubberduckvba.Server/Services/CacheService.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using rubberduckvba.Server.Api.Tags;
55
using rubberduckvba.Server.Data;
66
using rubberduckvba.Server.Model;
7+
using System.Security.Cryptography;
8+
using System.Text;
79

810
namespace rubberduckvba.Server.Services;
911

@@ -41,6 +43,9 @@ private void GetCurrentJobState()
4143
XmldocJobState = state.TryGetValue(XmldocJobName, out var xmldocsJobState) ? xmldocsJobState : null;
4244
}
4345

46+
public bool TryGetHtml(string markdown, out string? cached) => TryReadFromCache($"md:{Encoding.UTF8.GetString(SHA256.HashData(Encoding.UTF8.GetBytes(markdown)))}", out cached);
47+
public void CacheHtml(string markdown, string html) => Write($"md:{Encoding.UTF8.GetString(SHA256.HashData(Encoding.UTF8.GetBytes(markdown)))}", html);
48+
4449
public bool TryGetLatestTags(out LatestTagsViewModel? cached) => TryReadFromTagsCache("tags/latest", out cached);
4550
public bool TryGetAvailableDownloads(out AvailableDownload[]? cached) => TryReadFromTagsCache("downloads", out cached);
4651
public bool TryGetFeatures(out FeatureViewModel[]? cached) => TryReadXmldocCache("features", out cached);
@@ -177,4 +182,10 @@ private bool TryReadXmldocCache<T>(string key, out T? cached)
177182

178183
return result;
179184
}
185+
186+
private bool TryReadFromCache(string key, out string? cached)
187+
{
188+
var result = _cache.TryGetValue(key, out cached);
189+
return result;
190+
}
180191
}

rubberduckvba.Server/Services/GitHubClientService.cs

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,51 +42,52 @@ private class ReleaseComparer : IEqualityComparer<Release>
4242
var credentials = new Credentials(token);
4343
var client = new GitHubClient(new ProductHeaderValue(config.UserAgent), new InMemoryCredentialStore(credentials));
4444

45-
var user = await client.User.Current();
46-
var orgs = await client.Organization.GetAllForCurrent();
47-
48-
var org = orgs.SingleOrDefault(e => e.Id == RDConstants.Org.OrganisationId);
49-
var isOrgMember = org is Organization rdOrg;
50-
45+
var (name, role) = await DetermineUserRole(client);
5146
var claims = new List<Claim>
5247
{
53-
new(ClaimTypes.Name, user.Login),
48+
new(ClaimTypes.Name, name),
49+
new(ClaimTypes.Role, role),
5450
new(ClaimTypes.Authentication, token),
5551
new("access_token", token)
5652
};
5753

58-
if (isOrgMember && !user.Suspended)
54+
return new ClaimsPrincipal(new ClaimsIdentity(claims, "github"));
55+
}
56+
57+
private static async Task<(string name, string role)> DetermineUserRole(GitHubClient client)
58+
{
59+
var role = RDConstants.Roles.ReaderRole;
60+
61+
var user = await client.User.Current();
62+
if (!user.Suspended)
5963
{
60-
var teams = await client.Organization.Team.GetAllForCurrent();
64+
// only authenticated GitHub users in good standing can submit edits
65+
role = RDConstants.Roles.WriterRole;
6166

62-
var adminTeam = teams.SingleOrDefault(e => e.Name == RDConstants.Org.WebAdminTeam);
63-
if (adminTeam is not null)
67+
var orgs = await client.Organization.GetAllForCurrent();
68+
if (orgs.SingleOrDefault(e => e.Id == RDConstants.Org.OrganisationId) is not null)
6469
{
65-
// authenticated members of the org who are in the admin team can manage the site and approve their own changes
66-
claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.AdminRole));
67-
}
68-
else
69-
{
70-
var contributorsTeam = teams.SingleOrDefault(e => e.Name == RDConstants.Org.ContributorsTeam);
71-
if (contributorsTeam is not null)
72-
{
73-
// members of the contributors team can review/approve/reject suggested changes
74-
claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.ReviewerRole));
75-
}
76-
else
70+
var teams = await client.Organization.Team.GetAllForCurrent();
71+
72+
// members of the Rubberduck organization are welcome to review/approve/reject suggested changes
73+
// NOTE: opportunity for eventual distinction between members and contributors
74+
role = RDConstants.Roles.ReviewerRole;
75+
76+
//// members of the contributors team can review/approve/reject suggested changes
77+
//if (teams.SingleOrDefault(e => e.Name == RDConstants.Org.ContributorsTeam) is not null)
78+
//{
79+
// role = RDConstants.Roles.ReviewerRole;
80+
//}
81+
82+
// authenticated org members in the WebAdmin team can manage the site and approve their own changes
83+
if (teams.SingleOrDefault(e => e.Name == RDConstants.Org.WebAdminTeam) is not null)
7784
{
78-
// authenticated members of the org can submit edits
79-
claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.WriterRole));
85+
role = RDConstants.Roles.AdminRole;
8086
}
8187
}
8288
}
83-
else
84-
{
85-
claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.ReaderRole));
86-
}
8789

88-
var identity = new ClaimsIdentity(claims, "github");
89-
return new ClaimsPrincipal(identity);
90+
return (user.Login, role);
9091
}
9192

9293
public async Task<IEnumerable<TagGraph>> GetAllTagsAsync()

rubberduckvba.client/src/app/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { AuditFeatureDeleteComponent } from './components/audits/feature-delete.
4848
import { UserProfileComponent } from './routes/profile/user-profile.component';
4949
import { AuditItemComponent } from './routes/audits/audit-item/audit-item.component';
5050
import { AuthService } from './services/auth.service';
51+
import { EditBlogLinkComponent } from './components/edit-feature/edit-bloglink/edit-bloglink.component';
5152

5253
/**
5354
* https://stackoverflow.com/a/39560520
@@ -96,7 +97,8 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer {
9697
AnnotationComponent,
9798
QuickFixComponent,
9899
AboutComponent,
99-
EditFeatureComponent
100+
EditFeatureComponent,
101+
EditBlogLinkComponent
100102
],
101103
bootstrap: [AppComponent],
102104
imports: [
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div class="row my-2 p-1 bg-info-subtle">
2+
<div class="col-10">
3+
<div class="row">
4+
<div class="col-6">
5+
<input id="nameBox" title="Display title of the blog article" type="text" [(ngModel)]="blogLink.name" maxlength="255" [required]="true" class="p-1 w-100 border-0 fw-semibold" />
6+
</div>
7+
<div class="col-3">
8+
<input id="authorBox" title="Author of the linked article" type="text" [(ngModel)]="blogLink.author" maxlength="255" [required]="true" class="p-1 w-100 border-0" />
9+
</div>
10+
<div class="col-3">
11+
<input id="publishedBox" title="Publish date of the linked article" type="text" [(ngModel)]="blogLink.published" maxlength="10" [required]="true" class="p-1 w-100 border-0" />
12+
</div>
13+
</div>
14+
<div class="row mt-1">
15+
<div class="col-12">
16+
<input id="urlBox" title="URL to the blog article" type="text" [(ngModel)]="blogLink.url" maxlength="1023" [required]="true" class="p-1 w-100 border-0" />
17+
</div>
18+
</div>
19+
</div>
20+
<div class="col-2 align-content-center text-end">
21+
<button class="btn btn-danger m-1" (click)="onRemove()"><fa-icon [icon]="['fas', 'trash-can']"></fa-icon>&nbsp;Remove</button>
22+
</div>
23+
</div>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
2+
import { BlogLink } from "../../../model/feature.model";
3+
4+
@Component({
5+
selector: 'edit-blog-link',
6+
templateUrl: './edit-bloglink.component.html'
7+
})
8+
export class EditBlogLinkComponent implements OnInit {
9+
10+
private _blogLink: BlogLink = null!;
11+
12+
constructor() {
13+
14+
}
15+
16+
ngOnInit(): void {
17+
}
18+
19+
@Output()
20+
public removeLink = new EventEmitter<BlogLink>();
21+
22+
@Input()
23+
public set blogLink(value: BlogLink) {
24+
this._blogLink = value;
25+
}
26+
27+
public get blogLink(): BlogLink {
28+
return this._blogLink;
29+
}
30+
31+
public onRemove(): void {
32+
this.removeLink.emit(this.blogLink);
33+
}
34+
}

rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
</span>
1111
</button>
1212

13+
<button *ngIf="action == 'links'" type="button" class="btn btn-outline-secondary rounded-pill mx-1" (click)="doAction()" [disabled]="disabled">
14+
<span>
15+
<fa-icon [icon]="'link'"></fa-icon>&nbsp;Edit Links
16+
</span>
17+
</button>
18+
1319
<button *ngIf="action == 'create'" type="button" class="btn btn-outline-secondary rounded-pill m-1" (click)="doAction()" [disabled]="disabled">
1420
<span>
1521
<fa-icon [icon]="'plus-circle'"></fa-icon>&nbsp;Add Feature
@@ -30,13 +36,25 @@ <h4><img src="../../assets/vector-ducky-540.png" height="32">&nbsp;{{feature.tit
3036
</div>
3137
<div class="modal-body my-2 mx-4">
3238
<div class="row">
33-
<h6>Edit {{action == 'edit' ? 'description' : 'summary'}}</h6>
39+
<h6>Edit {{action == 'edit' ? 'description' : (action == 'summary' ? 'summary' : 'links')}}</h6>
3440
</div>
3541
<div class="row">
42+
3643
<textarea *ngIf="action == 'edit'" [(ngModel)]="feature.description" type="text" maxlength="8000" rows="10" class="font-monospace text-nowrap p-1">
3744
</textarea>
45+
3846
<textarea *ngIf="action == 'summary'" [(ngModel)]="feature.shortDescription" type="text" maxlength="1023" rows="5" class="font-monospace p-1">
3947
</textarea>
48+
49+
<div *ngIf="action == 'links'">
50+
<div *ngFor="let link of feature.links">
51+
<edit-blog-link [blogLink]="link" (removeLink)="onRemoveLink(link)"></edit-blog-link>
52+
</div>
53+
<div class="text-end">
54+
<button class="btn btn-outline-success" (click)="onAddLink()"><fa-icon [icon]="['fas', 'plus-circle']"></fa-icon>&nbsp;Add</button>
55+
</div>
56+
</div>
57+
4058
</div>
4159
<div class="row">
4260
<span *ngIf="action == 'edit'" class="text-small text-muted text-end">{{feature.description.length}}</span>

rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, TemplateRef, ViewChild, inject, input } from "@angular/core";
22
import { BehaviorSubject } from "rxjs";
3-
import { EditSubFeatureViewModelClass, MarkdownContent, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel } from "../../model/feature.model";
3+
import { BlogLink, BlogLinkViewModelClass, EditSubFeatureViewModelClass, MarkdownContent, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel } from "../../model/feature.model";
44
import { FaIconLibrary } from "@fortawesome/angular-fontawesome";
55
import { fas } from "@fortawesome/free-solid-svg-icons";
66
import { NgbModal } from "@ng-bootstrap/ng-bootstrap";
@@ -9,6 +9,7 @@ import { ApiClientService } from "../../services/api-client.service";
99
export enum AdminAction {
1010
Edit = 'edit',
1111
EditSummary = 'summary',
12+
EditLinks = 'links',
1213
Create = 'create',
1314
Delete = 'delete',
1415
}
@@ -98,14 +99,15 @@ export class EditFeatureComponent implements OnInit {
9899
featureTitle: parentTitle,
99100
isCollapsed: false,
100101
isDetailsCollapsed: true,
102+
links: []
101103
});
102104
}
103105

104106
this.modal.open(localModal, { modalDialogClass: size });
105107
}
106108

107109
public onConfirmChanges(): void {
108-
this.modal.dismissAll();
110+
this.modal.dismissAll();
109111
this.api.saveFeature(this.feature).subscribe(saved => {
110112
window.location.reload();
111113
});
@@ -138,4 +140,17 @@ export class EditFeatureComponent implements OnInit {
138140
window.location.reload();
139141
});
140142
}
143+
144+
public onRemoveLink(link: BlogLink): void {
145+
this.feature.links = this.feature.links!.filter(e => e.name.length > 0 && e.url.length > 0 && (e.name != link.name || e.url != link.url));
146+
}
147+
148+
public onAddLink(): void {
149+
this.feature.links.push(new BlogLinkViewModelClass({
150+
name: 'Title',
151+
author: 'Author',
152+
published: new Date().toISOString().substring(0, 10),
153+
url: 'https://rubberduckvba.blog/...'
154+
}));
155+
}
141156
}

rubberduckvba.client/src/app/components/feature-box/feature-box.component.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ <h4>{{feature.title}}</h4>
1414
</div>
1515

1616
<div class="row my-3" *ngIf="!hasOwnDetailsPage">
17-
<div class="col-12 text-center">
17+
<div class="col-1"></div>
18+
<div class="col-10 text-center">
1819
<button class="btn btn-outline-dark btn-ducky rounded-pill w-auto" role="button" data-toggle="collapse" data-target="#featureBoxDetailsBody" aria-controls="featureBoxDetailsBody" (click)="feature.isDetailsCollapsed = !feature.isDetailsCollapsed">
1920
<div *ngIf="feature.isDetailsCollapsed">
2021
Show more ▾
@@ -23,10 +24,13 @@ <h4>{{feature.title}}</h4>
2324
Show less ▴
2425
</div>
2526
</button>
26-
<div id="featureBoxDetailsBody" class="collapse" [ngClass]="{'show': !feature.isDetailsCollapsed}">
27-
<div [innerHtml]="feature.description"></div>
27+
<div id="featureBoxDetailsBody" class="collapse mt-3 text-start p-2" [ngClass]="{'show': !feature.isDetailsCollapsed}">
28+
<div class="card">
29+
<div class="card-body" [innerHtml]="descriptionHtml"></div>
30+
</div>
2831
</div>
2932
</div>
33+
<div class="col-1"></div>
3034
</div>
3135
</div>
3236
<div *ngIf="feature && user.isReviewer" class="card-footer">

0 commit comments

Comments
 (0)