Skip to content

Commit e76671a

Browse files
feat: add members settings page
1 parent c214f69 commit e76671a

File tree

6 files changed

+148
-2
lines changed

6 files changed

+148
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script lang="ts">
2+
import Button from "$lib/components/primitives/button/button.svelte";
3+
import type { User } from "$lib/model/backend";
4+
import { getName } from "$lib/utils/common-helper";
5+
import { cn } from "$lib/utils/shadcn-helper";
6+
import Trash from "lucide-svelte/icons/trash";
7+
8+
interface Props {
9+
member: User;
10+
isCurrentUser: boolean;
11+
isInvitationPending: boolean;
12+
isAdminView: boolean;
13+
}
14+
15+
let { member, isCurrentUser, isInvitationPending, isAdminView }: Props = $props();
16+
const role = member.isAdmin ? "Admin" : "Member";
17+
const isRoleReadonly = isCurrentUser || !isAdminView;
18+
</script>
19+
20+
<div class="flex flex-col sm:flex-row items-center justify-between px-4 gap-4">
21+
<div class="flex flex-col">
22+
<h3>
23+
{getName(member)}
24+
{#if isCurrentUser}
25+
<span class="text-gray-400"> - You</span>
26+
{/if}
27+
</h3>
28+
<span class="text-hint text-primary">{member.email}</span>
29+
</div>
30+
<div class="flex flex-row gap-2.5 items-center">
31+
{#if isInvitationPending}
32+
<span class="text-hint">Invitation Pending ...</span>
33+
{/if}
34+
<Button
35+
class={cn(
36+
"w-[7.7rem]",
37+
isRoleReadonly
38+
? "hover:bg-transparent hover:cursor-default"
39+
: "bg-gray-100 border border-gray-300 hover:bg-gray-200",
40+
)}
41+
variant={isRoleReadonly ? "ghost" : "secondary"}
42+
>
43+
<!-- Take maximum space so that text is left aligned -->
44+
<span class="flex w-full">Role: {role}</span>
45+
</Button>
46+
{#if isAdminView}
47+
<Button
48+
class="bg-gray-100 border border-gray-300 hover:bg-gray-200 size-10"
49+
disabled={isCurrentUser}
50+
>
51+
<Trash class=" text-red-500 !size-6" />
52+
</Button>
53+
{/if}
54+
</div>
55+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script lang="ts">
2+
import { Skeleton } from "$lib/components/primitives/skeleton";
3+
</script>
4+
5+
<div class="flex flex-col sm:flex-row items-center justify-between px-4 gap-4">
6+
<div class="flex flex-col gap-1">
7+
<Skeleton class="h-6 w-40 rounded-full" />
8+
<Skeleton class="h-5 w-52 rounded-full" />
9+
</div>
10+
<Skeleton class="h-7 w-40 rounded-full" />
11+
</div>

src/routes/project/[projectId]/settings/members/+page.svelte

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
<script lang="ts">
2+
import ProjectMemberListEntry from "$lib/components/composites/project-components/settings/members/ProjectMemberListEntry.svelte";
3+
import ProjectMemberListEntrySkeleton from "$lib/components/composites/project-components/settings/members/ProjectMemberListEntrySkeleton.svelte";
24
import ProjectSettingsLayout from "$lib/components/composites/project-components/settings/ProjectSettingsLayout.svelte";
5+
import Separator from "$lib/components/primitives/separator/separator.svelte";
6+
import Button from "$lib/components/primitives/button/button.svelte";
37
48
let { data } = $props();
5-
const { projectId, loadingProject } = data;
9+
const { user, projectId, loadingProject, loadingMembers } = data;
10+
const numberOfSkeletons = 7;
611
</script>
712

813
<svelte:head>
@@ -15,4 +20,47 @@
1520
{/await}
1621
</svelte:head>
1722

18-
<ProjectSettingsLayout {projectId} selectedTab="members">Members content</ProjectSettingsLayout>
23+
<ProjectSettingsLayout {projectId} selectedTab="members">
24+
{#if user.isAdmin}
25+
<div class="flex flex-row items-center justify-between">
26+
<h1>Manage Access</h1>
27+
<Button>Add User</Button>
28+
</div>
29+
{:else}
30+
<h1>Members</h1>
31+
{/if}
32+
<div class="flex flex-col w-full h-fit rounded-md border gap-3 py-2.5">
33+
{#await loadingMembers}
34+
{#each { length: numberOfSkeletons }, i}
35+
<ProjectMemberListEntrySkeleton />
36+
{#if i < numberOfSkeletons - 1}
37+
<Separator />
38+
{/if}
39+
{/each}
40+
{:then members}
41+
{#each members as member, i}
42+
<!-- TODO: Replace isInvitationPending with actual value -->
43+
<ProjectMemberListEntry
44+
{member}
45+
isCurrentUser={member.id === user.id}
46+
isInvitationPending={i === 2}
47+
isAdminView={user.isAdmin}
48+
/>
49+
{#if i < members.length - 1}
50+
<Separator />
51+
{/if}
52+
{/each}
53+
{#if members.length === 0}
54+
<div class="m-auto py-1">
55+
<span class="text-hint">No members found</span>
56+
</div>
57+
{/if}
58+
{:catch error}
59+
<div class="m-auto py-4">
60+
<span class="text-error">
61+
Loading Project Members failed{error ? `: ${error.message}` : "."}
62+
</span>
63+
</div>
64+
{/await}
65+
</div>
66+
</ProjectSettingsLayout>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { BackendController } from "$lib/controller/backend-controller";
2+
import type { PageLoad } from "./$types";
3+
4+
export const load: PageLoad = ({ params }) => {
5+
const projectId = Number(params.projectId);
6+
const projectController = BackendController.getInstance().project(projectId);
7+
const loadingMembers = projectController.getMembers();
8+
9+
// attach noop-catch to handle promise rejection correctly (see https://svelte.dev/docs/kit/load#Streaming-with-promises)
10+
loadingMembers.catch(() => {});
11+
12+
return {
13+
loadingMembers,
14+
};
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import ProjectMemberListEntrySkeleton from "$lib/components/composites/project-components/settings/members/ProjectMemberListEntrySkeleton.svelte";
2+
import { render, screen } from "@testing-library/svelte";
3+
import { describe, expect, test } from "vitest";
4+
5+
describe("ProjectMembersListEntrySkeleton", () => {
6+
test("When all props are provided, then component is rendered correctly", () => {
7+
render(ProjectMemberListEntrySkeleton);
8+
9+
const skeletons = screen.queryAllByTestId("skeleton");
10+
expect(skeletons).toHaveLength(3);
11+
});
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { describe, test } from "vitest";
2+
3+
describe("ProjectMembersListEntry", () => {
4+
test("When all props are provided, then component is rendered correctly", () => {});
5+
});

0 commit comments

Comments
 (0)