diff --git a/apps/server/src/modules/workspaces/workspaces.controller.ts b/apps/server/src/modules/workspaces/workspaces.controller.ts
index 62c02481..31c0bd8f 100644
--- a/apps/server/src/modules/workspaces/workspaces.controller.ts
+++ b/apps/server/src/modules/workspaces/workspaces.controller.ts
@@ -102,6 +102,18 @@ export class WorkspacesController {
);
}
+ @Post('suspend')
+ @UseGuards(AuthGuard, AdminGuard)
+ async suspendUser(
+ @WorkspaceD() workspaceId: string,
+ @Body() userBody: UserBody,
+ ) {
+ return await this.workspacesService.suspendUser(
+ workspaceId,
+ userBody.userId,
+ );
+ }
+
@Post('add_users')
@UseGuards(AuthGuard)
async addUserToWorkspace(
diff --git a/apps/server/src/modules/workspaces/workspaces.service.ts b/apps/server/src/modules/workspaces/workspaces.service.ts
index f10f746c..ca5d8d56 100644
--- a/apps/server/src/modules/workspaces/workspaces.service.ts
+++ b/apps/server/src/modules/workspaces/workspaces.service.ts
@@ -367,4 +367,25 @@ export default class WorkspacesService {
);
res.status(200).json(invite);
}
+
+ async suspendUser(workspaceId: string, userId: string) {
+ const userOnWorkspace =
+ await this.prisma.usersOnWorkspaces.findUniqueOrThrow({
+ where: {
+ userId_workspaceId: {
+ workspaceId,
+ userId,
+ },
+ },
+ });
+
+ await this.prisma.usersOnWorkspaces.update({
+ where: {
+ id: userOnWorkspace.id,
+ },
+ data: {
+ status: 'SUSPENDED',
+ },
+ });
+ }
}
diff --git a/apps/webapp/src/common/types/workspace.ts b/apps/webapp/src/common/types/workspace.ts
index b0af0f13..c1278b01 100644
--- a/apps/webapp/src/common/types/workspace.ts
+++ b/apps/webapp/src/common/types/workspace.ts
@@ -17,6 +17,7 @@ export interface UsersOnWorkspaceType {
createdAt: string;
updatedAt: string;
role: string;
+ status: string;
userId: string;
workspaceId: string;
diff --git a/apps/webapp/src/modules/ai/conversation-item.tsx b/apps/webapp/src/modules/ai/conversation-item.tsx
index 9df5ac3a..63ebee09 100644
--- a/apps/webapp/src/modules/ai/conversation-item.tsx
+++ b/apps/webapp/src/modules/ai/conversation-item.tsx
@@ -1,6 +1,6 @@
import { UserTypeEnum } from '@tegonhq/types';
import { Button } from '@tegonhq/ui/components/button';
-import { defaultExtensions } from '@tegonhq/ui/components/ui/editor/editor-extensions';
+import { defaultExtensions } from '@tegonhq/ui/components/editor/editor-extensions';
import { AI } from '@tegonhq/ui/icons';
import { Editor } from '@tiptap/core';
import { observer } from 'mobx-react-lite';
diff --git a/apps/webapp/src/modules/ai/conversation-textarea.tsx b/apps/webapp/src/modules/ai/conversation-textarea.tsx
index a1f5ab15..e682924f 100644
--- a/apps/webapp/src/modules/ai/conversation-textarea.tsx
+++ b/apps/webapp/src/modules/ai/conversation-textarea.tsx
@@ -1,5 +1,5 @@
-import { AdjustableTextArea } from '@tegonhq/ui/components/ui/adjustable-textarea';
-import { Button } from '@tegonhq/ui/components/ui/button';
+import { AdjustableTextArea } from '@tegonhq/ui/components/adjustable-textarea';
+import { Button } from '@tegonhq/ui/components/button';
import { SendLine } from '@tegonhq/ui/icons';
import { useState } from 'react';
diff --git a/apps/webapp/src/modules/ai/conversation.tsx b/apps/webapp/src/modules/ai/conversation.tsx
index 3d2d0e06..7917246e 100644
--- a/apps/webapp/src/modules/ai/conversation.tsx
+++ b/apps/webapp/src/modules/ai/conversation.tsx
@@ -1,5 +1,5 @@
import { UserTypeEnum } from '@tegonhq/types';
-import { ScrollArea } from '@tegonhq/ui/components/ui/scroll-area';
+import { ScrollArea } from '@tegonhq/ui/components/scroll-area';
import { cn } from '@tegonhq/ui/lib/utils';
import { observer } from 'mobx-react-lite';
import getConfig from 'next/config';
diff --git a/apps/webapp/src/modules/issues/new-issue/new-issue-header.tsx b/apps/webapp/src/modules/issues/new-issue/new-issue-header.tsx
index 7f82960d..ce05cfca 100644
--- a/apps/webapp/src/modules/issues/new-issue/new-issue-header.tsx
+++ b/apps/webapp/src/modules/issues/new-issue/new-issue-header.tsx
@@ -1,4 +1,4 @@
-import { Separator } from '@tegonhq/ui/components/ui/separator';
+import { Separator } from '@tegonhq/ui/components/separator';
import { observer } from 'mobx-react-lite';
import type { IssueType, TeamType } from 'common/types';
diff --git a/apps/webapp/src/modules/issues/single-issue/left-side/file-upload/file-upload.tsx b/apps/webapp/src/modules/issues/single-issue/left-side/file-upload/file-upload.tsx
index aec8ad50..dd109c00 100644
--- a/apps/webapp/src/modules/issues/single-issue/left-side/file-upload/file-upload.tsx
+++ b/apps/webapp/src/modules/issues/single-issue/left-side/file-upload/file-upload.tsx
@@ -1,6 +1,6 @@
import { Button } from '@tegonhq/ui/components/button';
+import { useEditor } from '@tegonhq/ui/components/editor/editor';
import { uploadFileFn, uploadFn } from '@tegonhq/ui/components/editor/utils';
-import { useEditor } from '@tegonhq/ui/components/ui/editor/editor';
import { Paperclip } from 'lucide-react';
interface FileUploadProps {
diff --git a/apps/webapp/src/modules/settings/team-settings/members.tsx b/apps/webapp/src/modules/settings/team-settings/members.tsx
index 58b654cc..541ae835 100644
--- a/apps/webapp/src/modules/settings/team-settings/members.tsx
+++ b/apps/webapp/src/modules/settings/team-settings/members.tsx
@@ -11,6 +11,7 @@ import { useCurrentTeam } from 'hooks/teams';
import { useUsersData } from 'hooks/users';
import { useContextStore } from 'store/global-context-provider';
+import { UserContext } from 'store/user-context';
import { ShowMembersDropdown } from './show-members-dropdown';
import { SettingSection } from '../setting-section';
@@ -19,10 +20,16 @@ export const Members = observer(() => {
const team = useCurrentTeam();
const { workspaceStore } = useContextStore();
const usersOnWorkspace = workspaceStore.usersOnWorkspaces;
+ const currentUser = React.useContext(UserContext);
+ const userRole = workspaceStore.getUserData(currentUser.id).role;
const userIds = usersOnWorkspace
.filter((uOW: UsersOnWorkspaceType) => {
- return uOW.teamIds.includes(team.id) && uOW.role !== RoleEnum.BOT;
+ return (
+ uOW.teamIds.includes(team.id) &&
+ uOW.role !== RoleEnum.BOT &&
+ uOW.status !== 'SUSPENDED'
+ );
})
.map((uOW: UsersOnWorkspaceType) => uOW.userId);
@@ -56,6 +63,7 @@ export const Members = observer(() => {
id={userData.id}
name={userData.fullname}
email={userData.email}
+ isAdmin={userRole === 'ADMIN'}
teamId={team.id}
className={
index === users.length - 1 && 'pb-0 !border-b-0'
diff --git a/apps/webapp/src/modules/settings/team-settings/overview/preferences.tsx b/apps/webapp/src/modules/settings/team-settings/overview/preferences.tsx
index 2434355f..8ecb6654 100644
--- a/apps/webapp/src/modules/settings/team-settings/overview/preferences.tsx
+++ b/apps/webapp/src/modules/settings/team-settings/overview/preferences.tsx
@@ -1,7 +1,7 @@
import { RiClipboardLine } from '@remixicon/react';
import { Button } from '@tegonhq/ui/components/button';
import { Input } from '@tegonhq/ui/components/input';
-import { useToast } from '@tegonhq/ui/components/ui/use-toast';
+import { useToast } from '@tegonhq/ui/components/use-toast';
import copy from 'copy-to-clipboard';
import { observer } from 'mobx-react-lite';
diff --git a/apps/webapp/src/modules/settings/workspace-settings/labels/labels.tsx b/apps/webapp/src/modules/settings/workspace-settings/labels/labels.tsx
index 3cb0a9a6..b9fff23d 100644
--- a/apps/webapp/src/modules/settings/workspace-settings/labels/labels.tsx
+++ b/apps/webapp/src/modules/settings/workspace-settings/labels/labels.tsx
@@ -62,7 +62,8 @@ export const Labels = observer(() => {
{labelsStore.labels
.filter(
(label: LabelType) =>
- label.name.includes(searchValue) && !label.teamId,
+ label.name.toLowerCase().includes(searchValue.toLowerCase()) &&
+ !label.teamId,
)
.map((label: LabelType) => {
if (editLabelState === label.id) {
diff --git a/apps/webapp/src/modules/settings/workspace-settings/members/member-item.tsx b/apps/webapp/src/modules/settings/workspace-settings/members/member-item.tsx
index 694eb34e..627556ab 100644
--- a/apps/webapp/src/modules/settings/workspace-settings/members/member-item.tsx
+++ b/apps/webapp/src/modules/settings/workspace-settings/members/member-item.tsx
@@ -15,10 +15,20 @@ interface MemberItemProps {
email: string;
id: string;
teamId?: string;
+ isAdmin?: boolean;
+ isSuspended?: boolean;
}
export const MemberItem = observer(
- ({ name, className, email, id, teamId }: MemberItemProps) => {
+ ({
+ name,
+ className,
+ email,
+ id,
+ teamId,
+ isAdmin,
+ isSuspended,
+ }: MemberItemProps) => {
const { workspaceStore } = useContextStore();
const userOnWorkspace = workspaceStore.usersOnWorkspaces.find(
@@ -29,7 +39,8 @@ export const MemberItem = observer(
@@ -44,12 +55,12 @@ export const MemberItem = observer(
{userOnWorkspace?.role}
- {teamId && (
-
- )}
+
);
diff --git a/apps/webapp/src/modules/settings/workspace-settings/members/member-options-dropdown.tsx b/apps/webapp/src/modules/settings/workspace-settings/members/member-options-dropdown.tsx
index aba9f1ed..26fba8ae 100644
--- a/apps/webapp/src/modules/settings/workspace-settings/members/member-options-dropdown.tsx
+++ b/apps/webapp/src/modules/settings/workspace-settings/members/member-options-dropdown.tsx
@@ -7,19 +7,24 @@ import {
DropdownMenuTrigger,
} from '@tegonhq/ui/components/dropdown-menu';
import { useToast } from '@tegonhq/ui/components/use-toast';
-import { DeleteLine, MoreLine } from '@tegonhq/ui/icons';
+import { CanceledLine, DeleteLine, MoreLine } from '@tegonhq/ui/icons';
import React from 'react';
import { useRemoveTeamMemberMutation } from 'services/team';
+import { useSuspendUserMutation } from 'services/workspace';
interface MemberOptionsDropdownProps {
userId: string;
teamId: string;
+ isAdmin: boolean;
+ isSuspended: boolean;
}
export function MemberOptionsDropdown({
userId,
teamId,
+ isAdmin,
+ isSuspended,
}: MemberOptionsDropdownProps) {
const { toast } = useToast();
const { mutate: removeMember } = useRemoveTeamMemberMutation({
@@ -32,6 +37,19 @@ export function MemberOptionsDropdown({
},
});
+ const { mutate: suspendUser } = useSuspendUserMutation({
+ onSuccess: () => {
+ toast({
+ title: 'Success',
+ description: 'User has been suspended',
+ });
+ },
+ });
+
+ if (!isAdmin || isSuspended) {
+ return null;
+ }
+
return (
@@ -48,18 +66,35 @@ export function MemberOptionsDropdown({
- {
- removeMember({
- userId,
- teamId,
- });
- }}
- >
-
- Remove from team
-
-
+ {isAdmin && (
+ <>
+ {
+ suspendUser({
+ userId,
+ });
+ }}
+ >
+
+ Suspend
+
+
+ >
+ )}
+ {teamId && isAdmin && (
+ {
+ removeMember({
+ userId,
+ teamId,
+ });
+ }}
+ >
+
+ Remove from team
+
+
+ )}
diff --git a/apps/webapp/src/modules/settings/workspace-settings/members/members.tsx b/apps/webapp/src/modules/settings/workspace-settings/members/members.tsx
index 5974cac9..7d0a3765 100644
--- a/apps/webapp/src/modules/settings/workspace-settings/members/members.tsx
+++ b/apps/webapp/src/modules/settings/workspace-settings/members/members.tsx
@@ -1,4 +1,5 @@
import { Button } from '@tegonhq/ui/components/button';
+import { Input } from '@tegonhq/ui/components/input';
import { Loader } from '@tegonhq/ui/components/loader';
import { observer } from 'mobx-react-lite';
import React from 'react';
@@ -9,12 +10,37 @@ import type { User } from 'common/types';
import { useUsersData } from 'hooks/users';
+import { useContextStore } from 'store/global-context-provider';
+import { UserContext } from 'store/user-context';
+
import { AddMemberDialog } from './add-member-dialog';
import { MemberItem } from './member-item';
export const Members = observer(() => {
const { users, isLoading } = useUsersData(false);
+ const { workspaceStore } = useContextStore();
const [newMemberDialog, setNewMemberDialog] = React.useState(false);
+ const [searchValue, setSearchValue] = React.useState('');
+ const currentUser = React.useContext(UserContext);
+ const userRole = workspaceStore.getUserData(currentUser.id)?.role;
+
+ const getUsers = (isSuspened: boolean = false) => {
+ const nonSuspendedUsers = users.filter((user) =>
+ isSuspened
+ ? workspaceStore.getUserData(user.id).status === 'SUSPENDED'
+ : workspaceStore.getUserData(user.id).status !== 'SUSPENDED',
+ );
+
+ if (searchValue) {
+ return nonSuspendedUsers.filter(
+ (user) =>
+ user.fullname.toLowerCase().includes(searchValue.toLowerCase()) ||
+ user.email.toLowerCase().includes(searchValue.toLowerCase()),
+ );
+ }
+
+ return nonSuspendedUsers;
+ };
return (
<>
@@ -34,20 +60,47 @@ export const Members = observer(() => {
>
Add member
-
{users.length} Members
+
+
+ setSearchValue(e.currentTarget.value)}
+ />
+
-
- {users.map((userData: User, index) => (
+
+ {getUsers().map((userData: User, index) => (
))}
+
+ {getUsers(true).length > 0 && (
+
+
Suspended
+
+ {getUsers(true).map((userData: User, index) => (
+
+ ))}
+
+ )}
)}
diff --git a/apps/webapp/src/services/workspace/index.ts b/apps/webapp/src/services/workspace/index.ts
index 9b313b66..d6756a30 100644
--- a/apps/webapp/src/services/workspace/index.ts
+++ b/apps/webapp/src/services/workspace/index.ts
@@ -3,3 +3,4 @@ export * from './invite-action';
export * from './update-workspace';
export * from './create-initial-resources';
export * from './update-workspace-preferences';
+export * from './suspend-user';
diff --git a/apps/webapp/src/services/workspace/suspend-user.tsx b/apps/webapp/src/services/workspace/suspend-user.tsx
new file mode 100644
index 00000000..f90add14
--- /dev/null
+++ b/apps/webapp/src/services/workspace/suspend-user.tsx
@@ -0,0 +1,43 @@
+import { suspendUser } from '@tegonhq/services';
+import { useMutation, useQueryClient } from 'react-query';
+
+import type { WorkspaceType } from 'common/types';
+
+import { GetUserQuery } from 'services/users';
+
+interface MutationParams {
+ onMutate?: () => void;
+ onSuccess?: (team: WorkspaceType) => void;
+ onError?: (error: string) => void;
+}
+
+export function useSuspendUserMutation({
+ onMutate,
+ onSuccess,
+ onError,
+}: MutationParams) {
+ const queryClient = useQueryClient();
+
+ const onMutationTriggered = () => {
+ onMutate && onMutate();
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const onMutationError = (errorResponse: any) => {
+ const errorText = errorResponse?.errors?.message || 'Error occured';
+
+ onError && onError(errorText);
+ };
+
+ const onMutationSuccess = (team: WorkspaceType) => {
+ queryClient.invalidateQueries({ queryKey: [GetUserQuery] });
+
+ onSuccess && onSuccess(team);
+ };
+
+ return useMutation(suspendUser, {
+ onError: onMutationError,
+ onMutate: onMutationTriggered,
+ onSuccess: onMutationSuccess,
+ });
+}
diff --git a/apps/webapp/src/store/database.ts b/apps/webapp/src/store/database.ts
index 24edc786..1a2bd4d2 100644
--- a/apps/webapp/src/store/database.ts
+++ b/apps/webapp/src/store/database.ts
@@ -54,7 +54,7 @@ export class TegonDatabase extends Dexie {
constructor(databaseName: string) {
super(databaseName);
- this.version(16).stores({
+ this.version(17).stores({
[MODELS.Workspace]: 'id,createdAt,updatedAt,name,slug,preferences',
[MODELS.Label]:
'id,createdAt,updatedAt,name,color,description,workspaceId,groupId,teamId',
@@ -65,7 +65,7 @@ export class TegonDatabase extends Dexie {
[MODELS.Issue]:
'id,createdAt,updatedAt,title,number,description,priority,dueDate,sortOrder,estimate,teamId,createdById,assigneeId,labelIds,parentId,stateId,sourceMetadata.projectId,projectMilestoneId,cycleId',
[MODELS.UsersOnWorkspaces]:
- 'id,createdAt,updatedAt,userId,workspaceId,teamIds,settings',
+ 'id,createdAt,updatedAt,userId,workspaceId,teamIds,settings,role,status',
[MODELS.IssueHistory]:
'id,createdAt,updatedAt,userId,issueId,assedLabelIds,removedLabelIds,fromPriority,toPriority,fromStateId,toStateId,fromEstimate,toEstimate,fromAssigneeId,toAssigneeId,fromParentId,toParentId,sourceMetadata',
[MODELS.IssueComment]:
diff --git a/apps/webapp/src/store/workspace/models.ts b/apps/webapp/src/store/workspace/models.ts
index dfccbd23..54aba23b 100644
--- a/apps/webapp/src/store/workspace/models.ts
+++ b/apps/webapp/src/store/workspace/models.ts
@@ -30,6 +30,10 @@ export const UsersOnWorkspace = types.model({
userId: types.string,
workspaceId: types.string,
role: types.enumeration(['ADMIN', 'USER', 'BOT', 'AGENT']),
+ status: types.union(
+ types.undefined,
+ types.enumeration(['INVITED', 'ACTIVE', 'SUSPENDED']),
+ ),
teamIds: types.array(types.string),
settings: types.union(
types.model({
diff --git a/apps/webapp/src/store/workspace/save-data.ts b/apps/webapp/src/store/workspace/save-data.ts
index 5b9cd0a5..c69e9454 100644
--- a/apps/webapp/src/store/workspace/save-data.ts
+++ b/apps/webapp/src/store/workspace/save-data.ts
@@ -20,6 +20,7 @@ export async function saveWorkspaceData(
workspaceId: record.data.workspaceId,
teamIds: record.data.teamIds,
role: record.data.role,
+ status: record.data.status,
settings: record.data.settings,
};
diff --git a/packages/services/src/workspace/index.ts b/packages/services/src/workspace/index.ts
index 063455ff..c6ebc84d 100644
--- a/packages/services/src/workspace/index.ts
+++ b/packages/services/src/workspace/index.ts
@@ -1 +1,2 @@
export * from './update-workspace-preferences';
+export * from './suspend-user';
diff --git a/packages/services/src/workspace/suspend-user.ts b/packages/services/src/workspace/suspend-user.ts
new file mode 100644
index 00000000..3bf7104f
--- /dev/null
+++ b/packages/services/src/workspace/suspend-user.ts
@@ -0,0 +1,7 @@
+import axios from 'axios';
+
+export async function suspendUser(updateData: { userId: string }) {
+ const response = await axios.post(`/api/v1/workspaces/suspend`, updateData);
+
+ return response.data;
+}