28
src/routes/teams/[id]/__layout.svelte
Normal file
28
src/routes/teams/[id]/__layout.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params }) => {
|
||||
const url = `/teams/${params.id}.json`;
|
||||
const res = await fetch(url);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (!data.permissions || Object.entries(data.permissions).length === 0) {
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/teams'
|
||||
};
|
||||
}
|
||||
return {
|
||||
stuff: {
|
||||
...data
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 302,
|
||||
redirect: '/teams'
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
55
src/routes/teams/[id]/index.json.ts
Normal file
55
src/routes/teams/[id]/index.json.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event, false);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
|
||||
try {
|
||||
const user = await db.prisma.user.findFirst({
|
||||
where: { id: userId, teams: { some: { id } } },
|
||||
include: { permission: true }
|
||||
});
|
||||
if (!user) {
|
||||
return {
|
||||
status: 401
|
||||
};
|
||||
}
|
||||
const permissions = await db.prisma.permission.findMany({
|
||||
where: { teamId: id },
|
||||
include: { user: { select: { id: true, email: true } } }
|
||||
});
|
||||
const team = await db.prisma.team.findUnique({ where: { id }, include: { permissions: true } });
|
||||
const invitations = await db.prisma.teamInvitation.findMany({ where: { teamId: team.id } });
|
||||
return {
|
||||
body: {
|
||||
team,
|
||||
permissions,
|
||||
invitations
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = event.params;
|
||||
const { name } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.prisma.team.update({ where: { id }, data: { name: { set: name } } });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
214
src/routes/teams/[id]/index.svelte
Normal file
214
src/routes/teams/[id]/index.svelte
Normal file
@@ -0,0 +1,214 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch, params }) => {
|
||||
const url = `/teams/${params.id}.json`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let permissions;
|
||||
export let team;
|
||||
export let invitations;
|
||||
import { page, session } from '$app/stores';
|
||||
import Explainer from '$lib/components/Explainer.svelte';
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { post } from '$lib/api';
|
||||
const { id } = $page.params;
|
||||
|
||||
let invitation = {
|
||||
teamName: team.name,
|
||||
email: null,
|
||||
permission: 'read'
|
||||
};
|
||||
let myPermission = permissions.find((u) => u.user.id === $session.uid).permission;
|
||||
function isAdmin(permission = myPermission) {
|
||||
if (myPermission === 'admin' || myPermission === 'owner') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function sendInvitation() {
|
||||
try {
|
||||
await post(`/teams/${id}/invitation/invite.json`, {
|
||||
teamId: team.id,
|
||||
teamName: invitation.teamName,
|
||||
email: invitation.email,
|
||||
permission: invitation.permission
|
||||
});
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function revokeInvitation(id) {
|
||||
try {
|
||||
await post(`/teams/${id}/invitation/revoke.json`, { id });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function removeFromTeam(uid) {
|
||||
try {
|
||||
await post(`/teams/${id}/remove/user.json`, { teamId: team.id, uid });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function changePermission(userId, permissionId, currentPermission) {
|
||||
let newPermission = 'read';
|
||||
if (currentPermission === 'read') {
|
||||
newPermission = 'admin';
|
||||
}
|
||||
try {
|
||||
await post(`/teams/${id}/permission/change.json`, { userId, newPermission, permissionId });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await post(`/teams/${id}.json`, { ...team });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 px-6 text-2xl font-bold">
|
||||
<div class="tracking-tight">Team</div>
|
||||
<span class="arrow-right-applications px-1 text-cyan-500">></span>
|
||||
<span class="pr-2">{team.name}</span>
|
||||
</div>
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<form on:submit|preventDefault={handleSubmit}>
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="title">Settings</div>
|
||||
<div class="text-center">
|
||||
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-2 flex items-center space-x-2 px-4 sm:px-6">
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" placeholder="name" bind:value={team.name} />
|
||||
</div>
|
||||
{#if team.id === '0'}
|
||||
<div class="px-20 pt-4 text-center">
|
||||
<Explainer
|
||||
maxWidthClass="w-full"
|
||||
text="This is the <span class='text-red-500 font-bold'>root</span> team. <br><br>That means members of this group can manage instance wide settings and have all the priviliges in Coolify. (imagine like root user on Linux)"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<div class="flex space-x-1 py-5 px-6 pt-10 font-bold">
|
||||
<div class="title">Members</div>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6">
|
||||
<table class="mx-2 w-full table-auto text-left">
|
||||
<tr class="h-8 border-b border-coolgray-400">
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Permission</th>
|
||||
<th scope="col" class="text-center">Actions</th>
|
||||
</tr>
|
||||
{#each permissions as permission}
|
||||
<tr class="text-xs">
|
||||
<td class="py-4"
|
||||
>{permission.user.email}
|
||||
<span class="font-bold">{permission.user.id === $session.uid ? '(You)' : ''}</span></td
|
||||
>
|
||||
<td class="py-4">{permission.permission}</td>
|
||||
{#if $session.isAdmin && permission.user.id !== $session.uid && permission.permission !== 'owner'}
|
||||
<td class="flex flex-col items-center justify-center space-y-2 py-4 text-center">
|
||||
<button
|
||||
class="w-52 bg-red-600 hover:bg-red-500"
|
||||
on:click={() => removeFromTeam(permission.user.id)}>Remove</button
|
||||
>
|
||||
<button
|
||||
class="w-52"
|
||||
on:click={() =>
|
||||
changePermission(permission.user.id, permission.id, permission.permission)}
|
||||
>Promote to {permission.permission === 'admin' ? 'read' : 'admin'}</button
|
||||
>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="text-center py-4 flex-col space-y-2"> No actions available </td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
{#each invitations as invitation}
|
||||
<tr class="text-xs">
|
||||
<td class="py-4 font-bold text-yellow-500">{invitation.email} </td>
|
||||
<td class="py-4 font-bold text-yellow-500">{invitation.permission}</td>
|
||||
{#if isAdmin(team.permissions[0].permission)}
|
||||
<td class="flex-col space-y-2 py-4 text-center">
|
||||
<button
|
||||
class="w-52 bg-red-600 hover:bg-red-500"
|
||||
on:click={() => revokeInvitation(invitation.id)}>Revoke invitation</button
|
||||
>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="text-center py-4 flex-col space-y-2">Pending invitation</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{#if $session.isAdmin}
|
||||
<div class="mx-auto max-w-2xl pt-8">
|
||||
<form on:submit|preventDefault={sendInvitation}>
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="title">Invite new member</div>
|
||||
<div class="text-center">
|
||||
<button class="bg-cyan-600 hover:bg-cyan-500" type="submit">Send invitation</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-col space-y-2 px-4 sm:px-6">
|
||||
<div class="flex space-x-0">
|
||||
<input
|
||||
bind:value={invitation.email}
|
||||
placeholder="Email address"
|
||||
class="mr-2 w-full"
|
||||
required
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
on:click={() => (invitation.permission = 'read')}
|
||||
class="rounded-none rounded-l"
|
||||
type="button"
|
||||
class:bg-pink-500={invitation.permission === 'read'}>Read</button
|
||||
>
|
||||
<button
|
||||
on:click={() => (invitation.permission = 'admin')}
|
||||
class="rounded-none rounded-r"
|
||||
type="button"
|
||||
class:bg-red-500={invitation.permission === 'admin'}>Admin</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
36
src/routes/teams/[id]/invitation/accept.json.ts
Normal file
36
src/routes/teams/[id]/invitation/accept.json.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = await event.request.json();
|
||||
|
||||
try {
|
||||
const invitation = await db.prisma.teamInvitation.findFirst({
|
||||
where: { uid: userId },
|
||||
rejectOnNotFound: true
|
||||
});
|
||||
await db.prisma.team.update({
|
||||
where: { id: invitation.teamId },
|
||||
data: { users: { connect: { id: userId } } }
|
||||
});
|
||||
await db.prisma.permission.create({
|
||||
data: {
|
||||
user: { connect: { id: userId } },
|
||||
permission: invitation.permission,
|
||||
team: { connect: { id: invitation.teamId } }
|
||||
}
|
||||
});
|
||||
await db.prisma.teamInvitation.delete({ where: { id } });
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
69
src/routes/teams/[id]/invitation/invite.json.ts
Normal file
69
src/routes/teams/[id]/invitation/invite.json.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
async function createInvitation({ email, uid, teamId, teamName, permission }) {
|
||||
return await db.prisma.teamInvitation.create({
|
||||
data: { email, uid, teamId, teamName, permission }
|
||||
});
|
||||
}
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { email, permission, teamId, teamName } = await event.request.json();
|
||||
|
||||
try {
|
||||
const userFound = await db.prisma.user.findUnique({ where: { email } });
|
||||
if (!userFound) {
|
||||
throw {
|
||||
error: `No user found with '${email}' email address.`
|
||||
};
|
||||
}
|
||||
const uid = userFound.id;
|
||||
// Invitation to yourself?!
|
||||
if (uid === userId) {
|
||||
throw {
|
||||
error: `Invitation to yourself? Whaaaaat?`
|
||||
};
|
||||
}
|
||||
const alreadyInTeam = await db.prisma.team.findFirst({
|
||||
where: { id: teamId, users: { some: { id: uid } } }
|
||||
});
|
||||
if (alreadyInTeam) {
|
||||
throw {
|
||||
error: `Already in the team.`
|
||||
};
|
||||
}
|
||||
const invitationFound = await db.prisma.teamInvitation.findFirst({ where: { uid, teamId } });
|
||||
if (invitationFound) {
|
||||
if (dayjs().toDate() < dayjs(invitationFound.createdAt).add(1, 'day').toDate()) {
|
||||
throw {
|
||||
error: 'Invitiation already pending on user confirmation.'
|
||||
};
|
||||
} else {
|
||||
await db.prisma.teamInvitation.delete({ where: { id: invitationFound.id } });
|
||||
await createInvitation({ email, uid, teamId, teamName, permission });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Invitiation sent.'
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
await createInvitation({ email, uid, teamId, teamName, permission });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
message: 'Invitiation sent.'
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
20
src/routes/teams/[id]/invitation/revoke.json.ts
Normal file
20
src/routes/teams/[id]/invitation/revoke.json.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import { dayjs } from '$lib/dayjs';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { id } = await event.request.json();
|
||||
try {
|
||||
await db.prisma.teamInvitation.delete({ where: { id } });
|
||||
return {
|
||||
status: 200
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
23
src/routes/teams/[id]/permission/change.json.ts
Normal file
23
src/routes/teams/[id]/permission/change.json.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { userId, newPermission, permissionId } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.prisma.permission.updateMany({
|
||||
where: { id: permissionId, userId },
|
||||
data: { permission: { set: newPermission } }
|
||||
});
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
24
src/routes/teams/[id]/remove/user.json.ts
Normal file
24
src/routes/teams/[id]/remove/user.json.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const post: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
const { teamId, uid } = await event.request.json();
|
||||
|
||||
try {
|
||||
await db.prisma.team.update({
|
||||
where: { id: teamId },
|
||||
data: { users: { disconnect: { id: uid } } }
|
||||
});
|
||||
await db.prisma.permission.deleteMany({ where: { userId: uid, teamId } });
|
||||
return {
|
||||
status: 201
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
26
src/routes/teams/index.json.ts
Normal file
26
src/routes/teams/index.json.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getUserDetails } from '$lib/common';
|
||||
import * as db from '$lib/database';
|
||||
import { PrismaErrorHandler } from '$lib/database';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
export const get: RequestHandler = async (event) => {
|
||||
const { userId, status, body } = await getUserDetails(event, false);
|
||||
if (status === 401) return { status, body };
|
||||
|
||||
try {
|
||||
const teams = await db.prisma.permission.findMany({
|
||||
where: { userId },
|
||||
include: { team: { include: { _count: { select: { users: true } } } } }
|
||||
});
|
||||
const invitations = await db.prisma.teamInvitation.findMany({ where: { uid: userId } });
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
teams,
|
||||
invitations
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return PrismaErrorHandler(error);
|
||||
}
|
||||
};
|
||||
113
src/routes/teams/index.svelte
Normal file
113
src/routes/teams/index.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script context="module" lang="ts">
|
||||
import type { Load } from '@sveltejs/kit';
|
||||
export const load: Load = async ({ fetch }) => {
|
||||
const url = `/teams.json`;
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
return {
|
||||
props: {
|
||||
...(await res.json())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: res.status,
|
||||
error: new Error(`Could not load ${url}`)
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { errorNotification } from '$lib/form';
|
||||
import { session } from '$app/stores';
|
||||
import { post } from '$lib/api';
|
||||
|
||||
export let teams;
|
||||
export let invitations;
|
||||
|
||||
async function acceptInvitation(id, teamId) {
|
||||
try {
|
||||
await post(`/teams/${teamId}/invitation/accept.json`, { id });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
async function revokeInvitation(id, teamId) {
|
||||
try {
|
||||
await post(`/teams/${teamId}/invitation/revoke.json`, { id });
|
||||
return window.location.reload();
|
||||
} catch ({ error }) {
|
||||
return errorNotification(error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="mr-4 text-2xl tracking-tight">Teams</div>
|
||||
{#if $session.isAdmin}
|
||||
<a href="/new/team" class="add-icon bg-cyan-600 hover:bg-cyan-500">
|
||||
<svg
|
||||
class="w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/></svg
|
||||
>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if invitations.length > 0}
|
||||
<div class="mx-auto max-w-2xl pb-10">
|
||||
<div class="flex space-x-1 p-6 font-bold">
|
||||
<div class="title">Pending invitations</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
{#each invitations as invitation}
|
||||
<div class="flex justify-center space-x-2">
|
||||
<div>
|
||||
Invited to <span class="font-bold text-pink-600">{invitation.teamName}</span> with
|
||||
<span class="font-bold text-rose-600">{invitation.permission}</span> permission.
|
||||
</div>
|
||||
<button
|
||||
class="hover:bg-green-500"
|
||||
on:click={() => acceptInvitation(invitation.id, invitation.teamId)}>Accept</button
|
||||
>
|
||||
<button
|
||||
class="hover:bg-red-600"
|
||||
on:click={() => revokeInvitation(invitation.id, invitation.teamId)}>Delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="flex flex-wrap justify-center">
|
||||
{#each teams as team}
|
||||
<a href="/teams/{team.teamId}" class="w-96 p-2 no-underline">
|
||||
<div
|
||||
class="box-selection relative"
|
||||
class:hover:bg-cyan-600={team.team?.id !== '0'}
|
||||
class:hover:bg-red-500={team.team?.id === '0'}
|
||||
>
|
||||
<div class="truncate text-center text-xl font-bold">{team.team.name}</div>
|
||||
<div class="text-center text-xs">
|
||||
({team.team?.id === '0' ? 'root team - ' : ''}{team.permission})
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-center">{team.team._count.users} member(s)</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user