Skip to content

Commit ea62b41

Browse files
committed
Fixes
1 parent 4286c25 commit ea62b41

File tree

13 files changed

+85
-81
lines changed

13 files changed

+85
-81
lines changed

code/client/src/ui/pages/workspace/components/ManageWorkspaceDialog.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@ import Dialog from '@ui/components/dialog/Dialog';
22
import { RxCross1 } from 'react-icons/rx';
33
import { MdManageAccounts } from 'react-icons/md';
44
import './ManageMembersDialog.scss';
5+
import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
56

67
type ManageWorkspaceDialogProps = {
7-
members: string[];
8+
workspace: WorkspaceMeta;
89
onAddMember: (email: string) => void;
910
onRemoveMember: (email: string) => void;
10-
isPrivate: boolean;
1111
toggleVisibility: () => void;
1212
};
1313

1414
function ManageWorkspaceDialog({
15-
members,
15+
workspace,
1616
onAddMember,
1717
onRemoveMember,
18-
isPrivate,
1918
toggleVisibility,
2019
}: ManageWorkspaceDialogProps) {
2120
return (
@@ -28,13 +27,13 @@ function ManageWorkspaceDialog({
2827
submitText="Add Member"
2928
extraContent={
3029
<div className="manage-workspace-dialog">
31-
<button onClick={toggleVisibility}>Make {isPrivate ? 'Public' : 'Private'}</button>
30+
<button onClick={toggleVisibility}>Make {workspace.isPrivate ? 'Public' : 'Private'}</button>
3231
<h4>Current Members</h4>
3332
<ul>
34-
{members?.map(member => (
33+
{workspace.members?.map((member: string) => (
3534
<li key={member}>
3635
<p>{member}</p>
37-
{members.length > 1 && (
36+
{workspace.members.length > 1 && (
3837
<button onClick={() => onRemoveMember(member)}>
3938
<RxCross1 />
4039
</button>

code/client/src/ui/pages/workspaces/components/WorkspaceContextMenu.tsx

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,41 @@
11
import PopupMenu from '@ui/components/popup-menu/PopupMenu';
2-
import { ReactNode, useEffect, useState } from 'react';
2+
import { ReactNode } from 'react';
33
import { MdDelete, MdEdit } from 'react-icons/md';
44
import { useAuth } from '@/contexts/auth/useAuth';
55
import ManageWorkspaceDialog from '@ui/pages/workspace/components/ManageWorkspaceDialog';
6+
import { WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
67

78
type WorkspaceContextMenuProps = {
9+
workspace: WorkspaceMeta;
810
children: ReactNode;
911
onRename: () => void;
1012
onDelete: () => void;
11-
onGetMembers: () => Promise<string[]>;
1213
onAddMember: (email: string) => Promise<void>;
1314
onRemoveMember: (email: string) => Promise<void>;
14-
isPrivate: boolean;
1515
toggleVisibility: () => Promise<void>;
1616
};
1717

1818
function WorkspaceContextMenu({
19+
workspace,
1920
children,
2021
onRename,
2122
onDelete,
22-
onGetMembers,
2323
onAddMember,
2424
onRemoveMember,
25-
isPrivate,
2625
toggleVisibility,
2726
}: WorkspaceContextMenuProps) {
28-
const [members, setMembers] = useState<string[]>([]);
29-
const [isMember, setIsMember] = useState(false);
3027
const { currentUser } = useAuth();
31-
32-
useEffect(() => {
33-
async function fetchMembers() {
34-
const members = await onGetMembers();
35-
setMembers(members);
36-
setIsMember(members.includes(currentUser?.email || ''));
37-
}
38-
fetchMembers();
39-
// eslint-disable-next-line react-hooks/exhaustive-deps
40-
}, [currentUser]);
41-
28+
const isMember = workspace.members.includes(currentUser?.email || '');
4229
return (
4330
<PopupMenu item={children} enabled={isMember}>
4431
<button onClick={onRename}>
4532
<MdEdit />
4633
Rename
4734
</button>
4835
<ManageWorkspaceDialog
49-
members={members}
36+
workspace={workspace}
5037
onAddMember={onAddMember}
5138
onRemoveMember={onRemoveMember}
52-
isPrivate={isPrivate}
5339
toggleVisibility={toggleVisibility}
5440
/>
5541
<button onClick={onDelete}>

code/client/src/ui/pages/workspaces/components/WorkspaceView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ function WorkspaceView({
2424
onSelect,
2525
onDelete,
2626
onRename,
27-
onGetMembers,
2827
onAddMember,
2928
onRemoveMember,
3029
toggleVisibility,
@@ -45,7 +44,7 @@ function WorkspaceView({
4544
<div className="table-row">
4645
<Checkbox checked={isSelected} onChange={onCheckboxSelected} onClick={e => e.stopPropagation()} />
4746
{component}
48-
<p>{workspace.members}</p>
47+
<p>{workspace.members.length}</p>
4948
<p>{formatDate(workspace.createdAt)}</p>
5049
<p>{workspace.isPrivate ? 'Private' : 'Public'}</p>
5150
</div>
@@ -54,12 +53,11 @@ function WorkspaceView({
5453
WorkspaceComponent
5554
) : (
5655
<WorkspaceContextMenu
56+
workspace={workspace}
5757
onRename={() => setIsEditing(true)}
5858
onDelete={onDelete}
59-
onGetMembers={onGetMembers}
6059
onAddMember={onAddMember}
6160
onRemoveMember={onRemoveMember}
62-
isPrivate={workspace.isPrivate}
6361
toggleVisibility={toggleVisibility}
6462
>
6563
<Link to={`/workspaces/${workspace.id}`}>{WorkspaceComponent}</Link>

code/server/src/controllers/http/handlers/workspacesHandlers.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function workspacesHandlers(services: Services, io: Server) {
2121
id,
2222
name,
2323
createdAt: new Date().toISOString(),
24-
members: 1,
24+
members: [req.user!.email],
2525
isPrivate,
2626
};
2727
io.emit('createdWorkspace', workspace);
@@ -48,7 +48,7 @@ function workspacesHandlers(services: Services, io: Server) {
4848
if (!wid) throw new InvalidParameterError('Workspace id is required');
4949
const newProps = req.body as Partial<WorkspaceMeta>;
5050
await services.workspaces.updateWorkspace(wid, newProps);
51-
io.emit('updatedWorkspace', { id: wid, ...newProps } as WorkspaceMeta);
51+
io.emit('updatedWorkspace', { id: wid, ...newProps });
5252
httpResponse.noContent(res).send();
5353
};
5454

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

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

8183
const searchWorkspaces = async (req: Request, res: Response) => {
8284
const { query, skip, limit } = req.query;
8385
const searchParams: SearchParams = getSearchParams({ query, skip, limit });
84-
const workspaces = await services.workspaces.searchWorkspaces(searchParams);
86+
const workspaces = await services.workspaces.searchWorkspaces(searchParams, req.user?.email);
8587
httpResponse.ok(res).json(workspaces);
8688
};
8789

code/server/src/databases/memory/MemoryResourcesDB.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ export class MemoryResourcesDB implements ResourcesRepository {
1515

1616
// create resource
1717
const id = uuid();
18+
const now = new Date().toISOString();
1819
workspace.resources[id] = {
1920
id,
2021
name,
2122
workspace: wid,
2223
type,
2324
parent: parent || wid,
2425
children: [],
25-
createdAt: new Date().toISOString(),
26-
updatedAt: new Date().toISOString(),
26+
createdAt: now,
27+
updatedAt: now,
2728
};
2829

2930
// update parent

code/server/src/databases/memory/MemoryUsersDB.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,10 @@ export class MemoryUsersDB implements UsersRepository {
2727
async getUsers(): Promise<User[]> {
2828
return Object.values(Memory.users);
2929
}
30+
31+
async getUserByEmail(email: string): Promise<User> {
32+
const user = Object.values(Memory.users).find(user => user.email === email);
33+
if (!user) throw new NotFoundError(`User not found`);
34+
return user;
35+
}
3036
}

code/server/src/databases/memory/MemoryWorkspacesDB.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,25 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {
1414

1515
async createWorkspace(name: string, isPrivate: boolean): Promise<string> {
1616
const id = uuid();
17+
const now = new Date().toISOString();
1718
const root: Resource = {
1819
id,
1920
name: 'root',
2021
workspace: id,
2122
type: ResourceType.FOLDER,
2223
parent: '',
2324
children: [],
24-
createdAt: '',
25-
updatedAt: '',
25+
createdAt: now,
26+
updatedAt: now,
2627
};
27-
const now = new Date().toISOString();
2828
Memory.workspaces[id] = { id, name, isPrivate, resources: { [id]: root }, createdAt: now, members: [] };
2929
return id;
3030
}
3131

3232
async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
3333
return Object.values(Memory.workspaces)
3434
.filter(workspace => (email ? workspace.members.includes(email) : !workspace.isPrivate))
35-
.map(props => {
36-
const workspace = omit(props, ['resources']);
37-
return { ...workspace, members: workspace.members?.length || 0 };
38-
});
35+
.map(props => omit(props, ['resources']));
3936
}
4037

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

65-
async addWorkspaceMember(wid: string, userId: string): Promise<void> {
62+
async addWorkspaceMember(wid: string, userId: string): Promise<string[]> {
6663
const workspace = Memory.workspaces[wid];
6764
if (!workspace) throw new NotFoundError(`Workspace not found`);
6865
Memory.workspaces[wid].members.push(userId);
66+
return Memory.workspaces[wid].members;
6967
}
7068

71-
async removeWorkspaceMember(wid: string, userId: string): Promise<void> {
69+
async removeWorkspaceMember(wid: string, userId: string): Promise<string[]> {
7270
const workspace = Memory.workspaces[wid];
7371
if (!workspace) throw new NotFoundError(`Workspace not found`);
7472
Memory.workspaces[wid].members = Memory.workspaces[wid].members.filter(member => member !== userId);
73+
return Memory.workspaces[wid].members;
7574
}
7675

77-
async searchWorkspaces(searchParams: SearchParams): Promise<WorkspaceMeta[]> {
76+
async searchWorkspaces(searchParams: SearchParams, email?: string): Promise<WorkspaceMeta[]> {
7877
const { query, skip, limit } = searchParams;
7978
return Object.values(Memory.workspaces)
80-
.filter(workspace => !workspace.isPrivate) // public workspaces
79+
.filter(workspace => !workspace.isPrivate || workspace.members.includes(email || '')) // filter accessible workspaces
8180
.filter(workspace => (query ? workspace.name.toLowerCase().includes(query.toLowerCase()) : true)) // search by name
8281
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) // sort results by creation date (newest first)
8382
.slice(skip, skip + limit) // paginate results
84-
.map(workspace => ({
85-
// convert to WorkspaceMeta
86-
...omit(workspace, ['resources']),
87-
members: workspace.members?.length || 0,
88-
}));
83+
.map(workspace => omit(workspace, ['resources'])); // convert to WorkspaceMeta
8984
}
9085
}

code/server/src/databases/postgres/PostgresUsersDB.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,14 @@ export class PostgresUsersDB implements UsersRepository {
3131
async getUsers(): Promise<User[]> {
3232
return await sql`select * from "user"`;
3333
}
34+
35+
async getUserByEmail(email: string): Promise<User> {
36+
const results: User[] = await sql`
37+
select *
38+
from "user"
39+
where email = ${email}
40+
`;
41+
if (isEmpty(results)) throw new NotFoundError('User not found');
42+
return results[0];
43+
}
3444
}

code/server/src/databases/postgres/PostgresWorkspacesDB.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
1818
}
1919

2020
async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
21-
const condition = email ? sql`${email} = any(members)` : sql`"isPrivate" = false`;
2221
const results = await sql`
2322
select row_to_json(t) as workspace
2423
from (
25-
select *, count(members) as members
24+
select *
2625
from workspace
27-
where ${condition}
26+
where ${email ? sql`${email} = any(members)` : sql`"isPrivate" = false`}
2827
group by id
2928
order by "createdAt" desc
3029
) as t
@@ -75,32 +74,34 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
7574
if (isEmpty(results)) throw new NotFoundError(`Workspace not found`);
7675
}
7776

78-
async addWorkspaceMember(wid: string, email: string): Promise<void> {
79-
const results = await sql`
77+
async addWorkspaceMember(wid: string, email: string): Promise<string[]> {
78+
const result = await sql`
8079
update workspace
8180
set members = array_append(members, ${email}::text)
8281
where id = ${wid} and not ${email} = any(members)
83-
returning id
82+
returning members
8483
`;
85-
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member already in workspace`);
84+
if (isEmpty(result)) throw new NotFoundError(`Workspace not found`);
85+
return result[0].members;
8686
}
8787

88-
async removeWorkspaceMember(wid: string, email: string): Promise<void> {
89-
const results = await sql`
88+
async removeWorkspaceMember(wid: string, email: string): Promise<string[]> {
89+
const result = await sql`
9090
update workspace
9191
set members = array_remove(members, ${email}::text)
9292
where id = ${wid}
93-
returning id
93+
returning members
9494
`;
95-
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member does not exist`);
95+
if (isEmpty(result)) throw new NotFoundError(`Workspace not found`);
96+
return result[0].members;
9697
}
9798

98-
async searchWorkspaces(searchParams: SearchParams): Promise<WorkspaceMeta[]> {
99+
async searchWorkspaces(searchParams: SearchParams, email?: string): Promise<WorkspaceMeta[]> {
99100
const { query, skip, limit } = searchParams;
100101
return sql`
101-
select *, array_length(members, 1) as members
102+
select *
102103
from workspace
103-
where "isPrivate" = false and name ilike ${'%' + query + '%'}
104+
where ("isPrivate" = false or ${email || ''} = any(members)) and name ilike ${'%' + query + '%'}
104105
order by "createdAt" desc
105106
offset ${skip} limit ${limit}
106107
`;

code/server/src/databases/types.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,22 +110,22 @@ export interface WorkspacesRepository {
110110
*/
111111
deleteWorkspace: (id: string) => Promise<void>;
112112
/**
113-
* Add a member to a workspace
113+
* Add a member to a workspace, returning the current list of members
114114
* @param wid
115115
* @param email
116116
*/
117-
addWorkspaceMember: (wid: string, email: string) => Promise<void>;
117+
addWorkspaceMember: (wid: string, email: string) => Promise<string[]>;
118118
/**
119-
* Remove a member from a workspace
119+
* Remove a member from a workspace, returning the current list of members
120120
* @param wid
121121
* @param email
122122
*/
123-
removeWorkspaceMember: (wid: string, email: string) => Promise<void>;
123+
removeWorkspaceMember: (wid: string, email: string) => Promise<string[]>;
124124
/**
125125
* Search workspaces by name
126126
* @param searchParams
127127
*/
128-
searchWorkspaces: (searchParams: SearchParams) => Promise<WorkspaceMeta[]>;
128+
searchWorkspaces: (searchParams: SearchParams, email?: string) => Promise<WorkspaceMeta[]>;
129129
}
130130

131131
export interface UsersRepository {
@@ -150,6 +150,10 @@ export interface UsersRepository {
150150
* Get all users from the database
151151
*/
152152
getUsers: () => Promise<User[]>;
153+
/**
154+
* Get a user by email
155+
*/
156+
getUserByEmail: (email: string) => Promise<User>;
153157
}
154158

155159
export interface CommitsRepository {

0 commit comments

Comments
 (0)