Skip to content

Commit 5bbd157

Browse files
committed
Add collaboration UI
1 parent 03818d0 commit 5bbd157

File tree

12 files changed

+388
-239
lines changed

12 files changed

+388
-239
lines changed

backend/NXTBackend.API.Core/Services/Implementation/GitService.cs

Lines changed: 153 additions & 166 deletions
Large diffs are not rendered by default.

backend/NXTBackend.API.Core/Services/Implementation/ProjectService.cs

Lines changed: 70 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,57 +17,82 @@ namespace NXTBackend.API.Core.Services.Implementation;
1717
/// </summary>
1818
public sealed class ProjectService : BaseService<Project>, IProjectService
1919
{
20-
private readonly IGitService _git;
21-
private readonly IDistributedCache _cache;
20+
private readonly IGitService _git;
21+
private readonly IDistributedCache _cache;
2222

23-
public ProjectService(DatabaseContext ctx, IGitService git, IDistributedCache cache) : base(ctx)
24-
{
25-
_git = git ?? throw new ArgumentNullException(nameof(git));
26-
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
23+
public ProjectService(DatabaseContext ctx, IGitService git, IDistributedCache cache) : base(ctx)
24+
{
25+
_git = git ?? throw new ArgumentNullException(nameof(git));
26+
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
2727

28-
DefineFilter<string>("slug", (q, slug) => q.Where(p => p.Slug == slug));
29-
DefineFilter<string>("name", (q, name) => q.Where(p => EF.Functions.Like(p.Name, $"%{name}%")));
30-
}
28+
DefineFilter<string>("slug", (q, slug) => q.Where(p => p.Slug == slug));
29+
DefineFilter<string>("name", (q, name) => q.Where(p => EF.Functions.Like(p.Name, $"%{name}%")));
30+
}
3131

32-
public async Task<Project> CreateProjectWithGit(Project project, Git git)
33-
{
34-
var newProject = await _context.Projects.AddAsync(project);
35-
newProject.Entity.GitInfoId = git.Id;
36-
await _context.SaveChangesAsync();
37-
return newProject.Entity;
38-
}
32+
public async Task<Project> CreateProjectWithGit(Project project, Git git)
33+
{
34+
var newProject = await _context.Projects.AddAsync(project);
35+
newProject.Entity.GitInfoId = git.Id;
36+
await _context.SaveChangesAsync();
37+
return newProject.Entity;
38+
}
3939

40-
/// <inheritdoc />
41-
public async Task<string> GetFileFromProject(Guid projectId, string file, string branch)
42-
{
43-
var project = await _context.Projects
44-
.Include(p => p.GitInfo)
45-
.FirstOrDefaultAsync(p => p.Id == projectId)
46-
?? throw new ServiceException(StatusCodes.Status404NotFound, "Project not found");
40+
/// <inheritdoc />
41+
public async Task<string> GetFileFromProject(Guid projectId, string file, string branch)
42+
{
43+
var project = await _context.Projects
44+
.Include(p => p.GitInfo)
45+
.FirstOrDefaultAsync(p => p.Id == projectId)
46+
?? throw new ServiceException(StatusCodes.Status404NotFound, "Project not found");
4747

48-
var cacheKey = $"{project.Id}:{branch}:{file}";
49-
var markdown = await _cache.GetStringAsync(cacheKey);
50-
if (markdown is null)
51-
{
52-
markdown = await _git.GetFile(project.GitInfo.Namespace, file, branch);
53-
await _cache.SetStringAsync(cacheKey, markdown, options: new ()
54-
{
55-
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
56-
});
57-
}
48+
var cacheKey = $"{project.Id}:{branch}:{file}";
49+
var markdown = await _cache.GetStringAsync(cacheKey);
50+
if (markdown is null)
51+
{
52+
markdown = await _git.GetFile(project.GitInfoId, file, branch);
53+
await _cache.SetStringAsync(cacheKey, markdown, options: new()
54+
{
55+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
56+
});
57+
}
5858

59-
return markdown;
60-
}
59+
return markdown;
60+
}
6161

62-
public Task<PaginatedList<Rubric>> GetRubric(Project project, PaginationParams pagination)
63-
{
64-
// Implementation using _context
65-
throw new NotImplementedException();
66-
}
62+
public Task<PaginatedList<Rubric>> GetRubric(Project project, PaginationParams pagination)
63+
{
64+
// Implementation using _context
65+
throw new NotImplementedException();
66+
}
6767

68-
public Task<PaginatedList<User>> GetUsers(Project project, PaginationParams pagination, SortingParams sorting)
69-
{
70-
// Implementation using _context
71-
throw new NotImplementedException();
72-
}
68+
public Task<PaginatedList<User>> GetUsers(Project project, PaginationParams pagination, SortingParams sorting)
69+
{
70+
// Implementation using _context
71+
throw new NotImplementedException();
72+
}
73+
74+
public async Task<(Project?, User?)> IsCollaborator(Guid entityId, Guid userId)
75+
{
76+
var project = await FindByIdAsync(entityId);
77+
if (project is null)
78+
return (null, null);
79+
// TODO: Do we want to count the creator always as a collaborator ?
80+
if (project.CreatorId == userId)
81+
return (project, project.Creator);
82+
83+
var (git, user) = await _git.IsCollaborator(project.GitInfoId, userId);
84+
if (git is null)
85+
throw new ServiceException(StatusCodes.Status422UnprocessableEntity, "Project has no linked git repository");
86+
return (project, user);
87+
}
88+
89+
public Task<bool> RemoveCollaborator(Guid entityId, Guid userId)
90+
{
91+
throw new NotImplementedException();
92+
}
93+
94+
public Task<bool> AddCollaborator(Guid entityId, Guid userId)
95+
{
96+
throw new NotImplementedException();
97+
}
7398
}

backend/NXTBackend.API.Core/Services/Interface/IGitService.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ public interface IGitService : IDomainService<Git>, ICollaborative<Git>
3232
/// - Creating a repo + project but project creation fails.
3333
/// </summary>
3434
/// <returns></returns>
35-
public Task DeleteRepository(string GitNamespace);
35+
public Task DeleteRepository(Guid id);
3636

3737
/// <summary>
3838
/// Updates certain Repository settings
3939
/// </summary>
4040
/// <param name="GitNamespace">The namespace to update</param>
4141
/// <param name="DTO">The Data Transfer Object describing the Repos update parameters.</param>
4242
/// <returns></returns>
43-
public Task<Git> UpdateRepository(string GitNamespace, GitRepoPatchRequestDTO DTO);
43+
public Task<Git> UpdateRepository(Guid id, GitRepoPatchRequestDTO DTO);
4444

4545
/// <summary>
4646
/// Get the file contents of a file in a given namespace and path.
@@ -49,7 +49,7 @@ public interface IGitService : IDomainService<Git>, ICollaborative<Git>
4949
/// <param name="Path"></param>
5050
/// <param name="Branch"></param>
5151
/// <returns></returns>
52-
public Task<string> GetFile(string GitNamespace, string Path, string Branch = "main");
52+
public Task<string> GetFile(Guid id, string Path, string Branch = "main");
5353

5454
/// <summary>
5555
/// Sets or updates a file in a Git repository with the specified content.
@@ -60,5 +60,5 @@ public interface IGitService : IDomainService<Git>, ICollaborative<Git>
6060
/// <param name="CommitMessage">The message describing the changes in the commit.</param>
6161
/// <param name="Branch">The branch where the file should be updated. Defaults to "main".</param>
6262
/// <returns>A task that represents the asynchronous operation.</returns>
63-
public Task SetFile(string GitNamespace, string Path, string Content, string CommitMessage, string Branch = "main");
63+
public Task SetFile(Guid id, string Path, string Content, string CommitMessage, string Branch = "main");
6464
}

backend/NXTBackend.API.Models/Requests/Project/ProjectPatchRequestDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ public class ProjectPatchRequestDto : BaseRequestDTO
6464
/// <summary>
6565
/// Tags for the project
6666
/// /// </summary>
67-
[MaxLength(24), StringLengthEnumerable(1, 64), JsonIgnore]
67+
// [MaxLength(24), StringLengthEnumerable(1, 64), JsonIgnore]
6868
public string[]? Tags { get; set; }
6969
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Text.Json.Serialization;
3+
4+
namespace NXTBackend.API.Models.Requests.ExternalGit;
5+
6+
public class RepoFileContentsDO
7+
{
8+
[JsonPropertyName("content"), Base64String]
9+
public string Content { get; set; }
10+
11+
[JsonPropertyName("sha")]
12+
public string Sha { get; set; }
13+
}

backend/NXTBackend.API.Models/Responses/Objects/GitDO.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// ============================================================================
55

66
using System.ComponentModel.DataAnnotations;
7+
using NXTBackend.API.Domain.Entities;
78
using NXTBackend.API.Domain.Enums;
89

910
// ============================================================================

backend/NXTBackend.API/Controllers/ProjectController.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public async Task<ActionResult<ProjectDO>> Create([FromBody] ProjectPostRequestD
7878
}, owner.Type);
7979

8080
await gitService.SetFile(
81-
git.Namespace,
81+
git.Id,
8282
"readme.md",
8383
data.Markdown,
8484
"Initial Commit"
@@ -106,10 +106,18 @@ await gitService.SetFile(
106106
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
107107
public async Task<ActionResult<ProjectDO>> Get(Guid id)
108108
{
109-
var project = await projectService.FindByIdAsync(id);
109+
// var project = await projectService.FindByIdAsync(id);
110+
111+
112+
var (project, user) = await projectService.IsCollaborator(id, User.GetSID());
113+
logger.LogInformation("==> Is User {UserId} a collaborator on project {ProjectId}: {isCollaborator}",
114+
User.GetSID(),
115+
id,
116+
user is not null
117+
);
118+
110119
if (project is null)
111120
return NotFound();
112-
113121
return Ok(new ProjectDO(project));
114122
}
115123

@@ -121,10 +129,10 @@ public async Task<ActionResult<ProjectDO>> Get(Guid id)
121129
")]
122130
[ProducesResponseType<string>(StatusCodes.Status200OK, contentType: "text/markdown")]
123131
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
132+
[OutputCache(Duration = 300)] // Cache for 5 minutes
124133
public async Task<ActionResult<string>> GetMarkdown(
125134
Guid id,
126135
string file,
127-
IDistributedCache cache,
128136
[FromQuery(Name = "filter[branch]")] string branch = "main"
129137
)
130138
{
@@ -143,28 +151,29 @@ public async Task<ActionResult<string>> GetMarkdown(
143151
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
144152
public async Task<ActionResult<ProjectDO>> Update(Guid id, [FromBody] ProjectPatchRequestDto data)
145153
{
146-
var project = await projectService.FindByIdAsync(id);
154+
var (project, user) = await projectService.IsCollaborator(id, User.GetSID());
147155
if (project is null)
148156
return NotFound();
149-
150-
151-
152-
153-
if (User.GetSID() != project.CreatorId)
157+
if (user is null)
154158
return Forbid();
155159

156160
// if (data.Markdown is not null)
157-
// project.Markdown = data.Markdown;
158-
if (data.Description is not null)
159-
project.Description = data.Description;
161+
// project.Markdown = data.Markdown;
162+
if (data.Description is not null)
163+
project.Description = data.Description;
160164
if (data.Name is not null)
161165
{
162166
project.Name = data.Name;
163167
project.Slug = project.Name.ToUrlSlug();
164168
}
165169

170+
if (data.Markdown is not null)
171+
{
172+
await gitService.SetFile(project.GitInfoId, "README.md", data.Markdown, "Update Readme");
173+
}
174+
166175
var updatedProject = await projectService.UpdateAsync(project);
167-
await gitService.UpsertFile(updatedProject.Creator.Login, project.Name, "README.md", data.Markdown, "Update README.md");
176+
// await gitService.UpsertFile(updatedProject.Creator.Login, project.Name, "README.md", data.Markdown, "Update README.md");
168177
return Ok(new ProjectDO(updatedProject));
169178
}
170179

backend/NXTBackend.API/appsettings.Development.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"Microsoft.AspNetCore": "Debug"
66
}
77
},
8+
"GitTemplate": "W2Wizard/Subject",
89
"ConnectionStrings": {
910
"DefaultConnection": "Host=31.187.76.65; Port=1337; Database=postgres; Username=postgres;",
1011
"Cache": "31.187.76.65:1338,ssl=False,abortConnect=False"

frontend/src/lib/components/search-api.svelte

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { Button } from "$lib/components/ui/button/index.js";
88
import { useDebounce } from "$lib/utils/debounce.svelte";
99
import Input from "./ui/input/input.svelte";
10+
import { cn } from "$lib/utils";
1011
1112
const debounce = useDebounce();
1213
let triggerRef = $state<HTMLButtonElement>(null!);
@@ -15,6 +16,7 @@
1516
interface Props {
1617
open?: boolean;
1718
placeholder?: string;
19+
class?: string;
1820
item: Snippet<[{ value: TData }]>;
1921
endpointFn: (search: string) => Promise<TData[]>;
2022
onSelect: (value: TData) => void | Promise<void>;
@@ -23,6 +25,7 @@
2325
let {
2426
open = $bindable(false),
2527
placeholder = "Search...",
28+
class: className,
2629
endpointFn,
2730
onSelect,
2831
item,
@@ -46,7 +49,7 @@
4649
{#snippet child({ props })}
4750
<Button
4851
variant="outline"
49-
class="w-[200px] justify-between"
52+
class={cn("w-[200px] justify-between text-muted-foreground ", className)}
5053
{...props}
5154
role="combobox"
5255
aria-expanded={open}
@@ -56,9 +59,8 @@
5659
</Button>
5760
{/snippet}
5861
</Popover.Trigger>
59-
<Popover.Content class="w-[350px] p-0">
60-
<div class="flex items-center border-b px-3">
61-
<Search class="opacity-50" />
62+
<Popover.Content class="w-[350px] p-0" align="start">
63+
<div class="flex border-b px-3">
6264
<Input
6365
oninput={(e) => debounce(searchFor, e.currentTarget.value.trim())}
6466
class="placeholder:text-muted-foreground flex h-9 w-full rounded-md border-none bg-transparent py-3 pl-1 text-base outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { json, redirect } from "@sveltejs/kit";
2+
import type { RequestHandler } from "./$types";
3+
import { logger } from "$lib/logger";
4+
5+
export const GET: RequestHandler = async ({ locals, url, fetch, request }) => {
6+
const { data, response } = await locals.api.GET("/users", {
7+
params: {
8+
query: {
9+
"filter[display_name]": url.searchParams.get("name") ?? undefined,
10+
},
11+
},
12+
});
13+
14+
logger.debug(response.status);
15+
return json(data);
16+
};

0 commit comments

Comments
 (0)