Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
GuilhermeF03 committed Jun 27, 2024
2 parents 646f7ae + ea62b41 commit 2d17f70
Show file tree
Hide file tree
Showing 13 changed files with 85 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@ import Dialog from '@ui/components/dialog/Dialog';
import { RxCross1 } from 'react-icons/rx';
import { MdManageAccounts } from 'react-icons/md';
import './ManageMembersDialog.scss';
import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';

type ManageWorkspaceDialogProps = {
members: string[];
workspace: WorkspaceMeta;
onAddMember: (email: string) => void;
onRemoveMember: (email: string) => void;
isPrivate: boolean;
toggleVisibility: () => void;
};

function ManageWorkspaceDialog({
members,
workspace,
onAddMember,
onRemoveMember,
isPrivate,
toggleVisibility,
}: ManageWorkspaceDialogProps) {
return (
Expand All @@ -28,13 +27,13 @@ function ManageWorkspaceDialog({
submitText="Add Member"
extraContent={
<div className="manage-workspace-dialog">
<button onClick={toggleVisibility}>Make {isPrivate ? 'Public' : 'Private'}</button>
<button onClick={toggleVisibility}>Make {workspace.isPrivate ? 'Public' : 'Private'}</button>
<h4>Current Members</h4>
<ul>
{members?.map(member => (
{workspace.members?.map((member: string) => (
<li key={member}>
<p>{member}</p>
{members.length > 1 && (
{workspace.members.length > 1 && (
<button onClick={() => onRemoveMember(member)}>
<RxCross1 />
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,41 @@
import PopupMenu from '@ui/components/popup-menu/PopupMenu';
import { ReactNode, useEffect, useState } from 'react';
import { ReactNode } from 'react';
import { MdDelete, MdEdit } from 'react-icons/md';
import { useAuth } from '@/contexts/auth/useAuth';
import ManageWorkspaceDialog from '@ui/pages/workspace/components/ManageWorkspaceDialog';
import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';

type WorkspaceContextMenuProps = {
workspace: WorkspaceMeta;
children: ReactNode;
onRename: () => void;
onDelete: () => void;
onGetMembers: () => Promise<string[]>;
onAddMember: (email: string) => Promise<void>;
onRemoveMember: (email: string) => Promise<void>;
isPrivate: boolean;
toggleVisibility: () => Promise<void>;
};

function WorkspaceContextMenu({
workspace,
children,
onRename,
onDelete,
onGetMembers,
onAddMember,
onRemoveMember,
isPrivate,
toggleVisibility,
}: WorkspaceContextMenuProps) {
const [members, setMembers] = useState<string[]>([]);
const [isMember, setIsMember] = useState(false);
const { currentUser } = useAuth();

useEffect(() => {
async function fetchMembers() {
const members = await onGetMembers();
setMembers(members);
setIsMember(members.includes(currentUser?.email || ''));
}
fetchMembers();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser]);

const isMember = workspace.members.includes(currentUser?.email || '');
return (
<PopupMenu item={children} enabled={isMember}>
<button onClick={onRename}>
<MdEdit />
Rename
</button>
<ManageWorkspaceDialog
members={members}
workspace={workspace}
onAddMember={onAddMember}
onRemoveMember={onRemoveMember}
isPrivate={isPrivate}
toggleVisibility={toggleVisibility}
/>
<button onClick={onDelete}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ function WorkspaceView({
onSelect,
onDelete,
onRename,
onGetMembers,
onAddMember,
onRemoveMember,
toggleVisibility,
Expand All @@ -45,7 +44,7 @@ function WorkspaceView({
<div className="table-row">
<Checkbox checked={isSelected} onChange={onCheckboxSelected} onClick={e => e.stopPropagation()} />
{component}
<p>{workspace.members}</p>
<p>{workspace.members.length}</p>
<p>{formatDate(workspace.createdAt)}</p>
<p>{workspace.isPrivate ? 'Private' : 'Public'}</p>
</div>
Expand All @@ -54,12 +53,11 @@ function WorkspaceView({
WorkspaceComponent
) : (
<WorkspaceContextMenu
workspace={workspace}
onRename={() => setIsEditing(true)}
onDelete={onDelete}
onGetMembers={onGetMembers}
onAddMember={onAddMember}
onRemoveMember={onRemoveMember}
isPrivate={workspace.isPrivate}
toggleVisibility={toggleVisibility}
>
<Link to={`/workspaces/${workspace.id}`}>{WorkspaceComponent}</Link>
Expand Down
12 changes: 7 additions & 5 deletions code/server/src/controllers/http/handlers/workspacesHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function workspacesHandlers(services: Services, io: Server) {
id,
name,
createdAt: new Date().toISOString(),
members: 1,
members: [req.user!.email],
isPrivate,
};
io.emit('createdWorkspace', workspace);
Expand All @@ -48,7 +48,7 @@ function workspacesHandlers(services: Services, io: Server) {
if (!wid) throw new InvalidParameterError('Workspace id is required');
const newProps = req.body as Partial<WorkspaceMeta>;
await services.workspaces.updateWorkspace(wid, newProps);
io.emit('updatedWorkspace', { id: wid, ...newProps } as WorkspaceMeta);
io.emit('updatedWorkspace', { id: wid, ...newProps });
httpResponse.noContent(res).send();
};

Expand All @@ -65,7 +65,8 @@ function workspacesHandlers(services: Services, io: Server) {
if (!wid) throw new InvalidParameterError('Workspace id is required');
const { email } = req.body;
if (!email) throw new InvalidParameterError('Email is required');
await services.workspaces.addWorkspaceMember(wid, email);
const members = await services.workspaces.addWorkspaceMember(wid, email);
io.emit('updatedWorkspace', { id: wid, members });
httpResponse.noContent(res).send();
};

Expand All @@ -74,14 +75,15 @@ function workspacesHandlers(services: Services, io: Server) {
if (!wid) throw new InvalidParameterError('Workspace id is required');
const { email } = req.body;
if (!email) throw new InvalidParameterError('Email is required');
await services.workspaces.removeWorkspaceMember(wid, email);
const members = await services.workspaces.removeWorkspaceMember(wid, email);
io.emit('updatedWorkspace', { id: wid, members });
httpResponse.noContent(res).send();
};

const searchWorkspaces = async (req: Request, res: Response) => {
const { query, skip, limit } = req.query;
const searchParams: SearchParams = getSearchParams({ query, skip, limit });
const workspaces = await services.workspaces.searchWorkspaces(searchParams);
const workspaces = await services.workspaces.searchWorkspaces(searchParams, req.user?.email);
httpResponse.ok(res).json(workspaces);
};

Expand Down
5 changes: 3 additions & 2 deletions code/server/src/databases/memory/MemoryResourcesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,16 @@ export class MemoryResourcesDB implements ResourcesRepository {

// create resource
const id = uuid();
const now = new Date().toISOString();
workspace.resources[id] = {
id,
name,
workspace: wid,
type,
parent: parent || wid,
children: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdAt: now,
updatedAt: now,
};

// update parent
Expand Down
6 changes: 6 additions & 0 deletions code/server/src/databases/memory/MemoryUsersDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,10 @@ export class MemoryUsersDB implements UsersRepository {
async getUsers(): Promise<User[]> {
return Object.values(Memory.users);
}

async getUserByEmail(email: string): Promise<User> {
const user = Object.values(Memory.users).find(user => user.email === email);
if (!user) throw new NotFoundError(`User not found`);
return user;
}
}
27 changes: 11 additions & 16 deletions code/server/src/databases/memory/MemoryWorkspacesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,25 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {

async createWorkspace(name: string, isPrivate: boolean): Promise<string> {
const id = uuid();
const now = new Date().toISOString();
const root: Resource = {
id,
name: 'root',
workspace: id,
type: ResourceType.FOLDER,
parent: '',
children: [],
createdAt: '',
updatedAt: '',
createdAt: now,
updatedAt: now,
};
const now = new Date().toISOString();
Memory.workspaces[id] = { id, name, isPrivate, resources: { [id]: root }, createdAt: now, members: [] };
return id;
}

async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
return Object.values(Memory.workspaces)
.filter(workspace => (email ? workspace.members.includes(email) : !workspace.isPrivate))
.map(props => {
const workspace = omit(props, ['resources']);
return { ...workspace, members: workspace.members?.length || 0 };
});
.map(props => omit(props, ['resources']));
}

async getWorkspace(id: string): Promise<Workspace> {
Expand All @@ -62,29 +59,27 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {
delete Memory.workspaces[id];
}

async addWorkspaceMember(wid: string, userId: string): Promise<void> {
async addWorkspaceMember(wid: string, userId: string): Promise<string[]> {
const workspace = Memory.workspaces[wid];
if (!workspace) throw new NotFoundError(`Workspace not found`);
Memory.workspaces[wid].members.push(userId);
return Memory.workspaces[wid].members;
}

async removeWorkspaceMember(wid: string, userId: string): Promise<void> {
async removeWorkspaceMember(wid: string, userId: string): Promise<string[]> {
const workspace = Memory.workspaces[wid];
if (!workspace) throw new NotFoundError(`Workspace not found`);
Memory.workspaces[wid].members = Memory.workspaces[wid].members.filter(member => member !== userId);
return Memory.workspaces[wid].members;
}

async searchWorkspaces(searchParams: SearchParams): Promise<WorkspaceMeta[]> {
async searchWorkspaces(searchParams: SearchParams, email?: string): Promise<WorkspaceMeta[]> {
const { query, skip, limit } = searchParams;
return Object.values(Memory.workspaces)
.filter(workspace => !workspace.isPrivate) // public workspaces
.filter(workspace => !workspace.isPrivate || workspace.members.includes(email || '')) // filter accessible workspaces
.filter(workspace => (query ? workspace.name.toLowerCase().includes(query.toLowerCase()) : true)) // search by name
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) // sort results by creation date (newest first)
.slice(skip, skip + limit) // paginate results
.map(workspace => ({
// convert to WorkspaceMeta
...omit(workspace, ['resources']),
members: workspace.members?.length || 0,
}));
.map(workspace => omit(workspace, ['resources'])); // convert to WorkspaceMeta
}
}
10 changes: 10 additions & 0 deletions code/server/src/databases/postgres/PostgresUsersDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,14 @@ export class PostgresUsersDB implements UsersRepository {
async getUsers(): Promise<User[]> {
return await sql`select * from "user"`;
}

async getUserByEmail(email: string): Promise<User> {
const results: User[] = await sql`
select *
from "user"
where email = ${email}
`;
if (isEmpty(results)) throw new NotFoundError('User not found');
return results[0];
}
}
29 changes: 15 additions & 14 deletions code/server/src/databases/postgres/PostgresWorkspacesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
}

async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
const condition = email ? sql`${email} = any(members)` : sql`"isPrivate" = false`;
const results = await sql`
select row_to_json(t) as workspace
from (
select *, count(members) as members
select *
from workspace
where ${condition}
where ${email ? sql`${email} = any(members)` : sql`"isPrivate" = false`}
group by id
order by "createdAt" desc
) as t
Expand Down Expand Up @@ -75,32 +74,34 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
if (isEmpty(results)) throw new NotFoundError(`Workspace not found`);
}

async addWorkspaceMember(wid: string, email: string): Promise<void> {
const results = await sql`
async addWorkspaceMember(wid: string, email: string): Promise<string[]> {
const result = await sql`
update workspace
set members = array_append(members, ${email}::text)
where id = ${wid} and not ${email} = any(members)
returning id
returning members
`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member already in workspace`);
if (isEmpty(result)) throw new NotFoundError(`Workspace not found`);
return result[0].members;
}

async removeWorkspaceMember(wid: string, email: string): Promise<void> {
const results = await sql`
async removeWorkspaceMember(wid: string, email: string): Promise<string[]> {
const result = await sql`
update workspace
set members = array_remove(members, ${email}::text)
where id = ${wid}
returning id
returning members
`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member does not exist`);
if (isEmpty(result)) throw new NotFoundError(`Workspace not found`);
return result[0].members;
}

async searchWorkspaces(searchParams: SearchParams): Promise<WorkspaceMeta[]> {
async searchWorkspaces(searchParams: SearchParams, email?: string): Promise<WorkspaceMeta[]> {
const { query, skip, limit } = searchParams;
return sql`
select *, array_length(members, 1) as members
select *
from workspace
where "isPrivate" = false and name ilike ${'%' + query + '%'}
where ("isPrivate" = false or ${email || ''} = any(members)) and name ilike ${'%' + query + '%'}
order by "createdAt" desc
offset ${skip} limit ${limit}
`;
Expand Down
14 changes: 9 additions & 5 deletions code/server/src/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,22 @@ export interface WorkspacesRepository {
*/
deleteWorkspace: (id: string) => Promise<void>;
/**
* Add a member to a workspace
* Add a member to a workspace, returning the current list of members
* @param wid
* @param email
*/
addWorkspaceMember: (wid: string, email: string) => Promise<void>;
addWorkspaceMember: (wid: string, email: string) => Promise<string[]>;
/**
* Remove a member from a workspace
* Remove a member from a workspace, returning the current list of members
* @param wid
* @param email
*/
removeWorkspaceMember: (wid: string, email: string) => Promise<void>;
removeWorkspaceMember: (wid: string, email: string) => Promise<string[]>;
/**
* Search workspaces by name
* @param searchParams
*/
searchWorkspaces: (searchParams: SearchParams) => Promise<WorkspaceMeta[]>;
searchWorkspaces: (searchParams: SearchParams, email?: string) => Promise<WorkspaceMeta[]>;
}

export interface UsersRepository {
Expand All @@ -150,6 +150,10 @@ export interface UsersRepository {
* Get all users from the database
*/
getUsers: () => Promise<User[]>;
/**
* Get a user by email
*/
getUserByEmail: (email: string) => Promise<User>;
}

export interface CommitsRepository {
Expand Down
Loading

0 comments on commit 2d17f70

Please sign in to comment.