Skip to content

Commit

Permalink
Finished Implementing Workspace Management
Browse files Browse the repository at this point in the history
  • Loading branch information
R1c4rdCo5t4 committed Jun 20, 2024
1 parent 3ec61a9 commit b462485
Show file tree
Hide file tree
Showing 34 changed files with 283 additions and 192 deletions.
7 changes: 1 addition & 6 deletions code/client/src/domain/editor/slate/operations/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,11 @@ export default (editor: Editor, connector: InputConnector, onFormat: (mark: Inli
function onInput(e: InputEvent) {
const key = getKeyFromInputEvent(e);
if (!key) return;

const selection = getSelection(editor);
console.log('Selection: ', selection);
const cursor = min([{ ...selection.start }, { ...selection.end }]) as Cursor; // always use the start of the selection
console.log('Initial Cursor: ', cursor);

// if there is a selection, delete the selected text
if (isSelected(editor)) connector.deleteSelection(selection);
console.log('Selection after deletion: ', getSelection(editor));
console.log('Cursor after deletion: ', cursor);
switch (key) {
case 'Enter':
onEnter(cursor);
Expand Down Expand Up @@ -81,7 +77,6 @@ export default (editor: Editor, connector: InputConnector, onFormat: (mark: Inli
*/
function onKey(key: string, cursor: Cursor) {
const styles = CustomEditor.getMarks(editor) as InlineStyle[];
console.log('Cursor: ', cursor);
connector.insertCharacter(key, cursor, styles);
}

Expand Down
3 changes: 2 additions & 1 deletion code/client/src/domain/workspaces/useWorkspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ function useWorkspaces() {
}

async function getWorkspaceMembers(id: string) {
return await service.getWorkspaceMembers(id);
const workspace = await service.getWorkspace(id);
return workspace.members;
}

async function addWorkspaceMember(id: string, email: string) {
Expand Down
34 changes: 13 additions & 21 deletions code/client/src/services/communication/http/httpCommunication.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,41 @@
import { SERVER_URL } from '@config';

export interface HttpCommunication {
post: (url: string, data?: any, withAuth?: boolean) => Promise<any>;
post: (url: string, data?: any) => Promise<any>;
get: (url: string, withAuth?: boolean) => Promise<any>;
put: (url: string, data?: any, withAuth?: boolean) => Promise<any>;
delete: (url: string, data?: any, withAuth?: boolean) => Promise<any>;
put: (url: string, data?: any) => Promise<any>;
delete: (url: string, data?: any) => Promise<any>;
}

async function get(url: string, withAuth: boolean = false) {
return request(url, 'GET', undefined, withAuth);
async function get(url: string) {
return request(url, 'GET', undefined);
}

async function post(url: string, body: any, withAuth: boolean = false) {
return request(url, 'POST', body, withAuth);
async function post(url: string, body?: any) {
return request(url, 'POST', body);
}

async function put(url: string, body: any, withAuth: boolean = false) {
return request(url, 'PUT', body, withAuth);
async function put(url: string, body?: any) {
return request(url, 'PUT', body);
}

async function del(url: string, body: any, withAuth: boolean = false) {
return request(url, 'DELETE', body, withAuth);
async function del(url: string, body?: any) {
return request(url, 'DELETE', body);
}

const request = async (url: string, method: string, body: any, withAuth: boolean) => {
const request = async (url: string, method: string, body?: any) => {
const requestInit: RequestInit = {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
};
if (body) requestInit.body = JSON.stringify(body);
if (withAuth) requestInit.credentials = 'include';

console.log('requestInit:', requestInit);

const response = await fetch(SERVER_URL + url, requestInit);
const noBody = response.status === 204 || response.headers.get('content-length') === '0';
if (noBody) return;
const result = await response.json();
if (response.ok) return result;
if (response.status === 401) {
throw new Error('Unauthorized');
}
if (response.status === 403) {
throw new Error('Forbidden');
}
throw new Error(result.error || 'Failed to fetch');
};

Expand Down
6 changes: 6 additions & 0 deletions code/client/src/services/workspace/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function validateEmail(email: string) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email');
}
}
10 changes: 3 additions & 7 deletions code/client/src/services/workspace/workspaceService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HttpCommunication } from '@services/communication/http/httpCommunication';
import { WorkspaceInputModel, WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import { Workspace } from '@notespace/shared/src/workspace/types/workspace';
import { UserData } from '@notespace/shared/src/users/types';
import { validateEmail } from '@services/workspace/utils';

function workspaceService(http: HttpCommunication) {
async function getWorkspace(id: string): Promise<Workspace> {
Expand All @@ -24,16 +24,13 @@ function workspaceService(http: HttpCommunication) {
await http.put(`/workspaces/${id}`, newProps);
}

async function getWorkspaceMembers(id: string): Promise<UserData[]> {
const workspace: WorkspaceMeta = await http.get(`/workspaces/${id}`);
return workspace.members;
}

async function addWorkspaceMember(id: string, email: string): Promise<void> {
validateEmail(email);
await http.post(`/workspaces/${id}/members`, { email });
}

async function removeWorkspaceMember(id: string, email: string): Promise<void> {
validateEmail(email);
await http.delete(`/workspaces/${id}/members`, { email });
}

Expand All @@ -43,7 +40,6 @@ function workspaceService(http: HttpCommunication) {
createWorkspace,
deleteWorkspace,
updateWorkspace,
getWorkspaceMembers,
addWorkspaceMember,
removeWorkspaceMember,
};
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function Dialog({ title, fields, onSubmit, submitText, extraContent, children }:
<MaterialDialog open={open} onClose={handleClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
{extraContent}
{fields.map(field =>
field.type === 'checkbox' ? (
<FormControlLabel
Expand Down Expand Up @@ -83,7 +84,6 @@ function Dialog({ title, fields, onSubmit, submitText, extraContent, children }:
/>
)
)}
{extraContent}
</DialogContent>
<DialogActions>
<button onClick={handleClose}>Cancel</button>
Expand Down
23 changes: 13 additions & 10 deletions code/client/src/ui/contexts/auth/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export type AuthContextType = {
loginWithGoogle: () => Promise<void>;
loginWithGithub: () => Promise<void>;
logout: () => Promise<void>;
deleteAccount: () => Promise<void>;
};

export const AuthContext = createContext<AuthContextType>({
currentUser: null,
loginWithGoogle: async () => {},
loginWithGithub: async () => {},
logout: async () => {},
deleteAccount: async () => {},
});

type AuthProviderProps = {
Expand All @@ -33,7 +35,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
try {
const { user } = await signInWithPopup(auth, provider);
await registerUser(user.uid, { name: user.displayName!, email: user.email! });
const token = await user.getIdToken();
const token = await user.getIdToken(true);
Cookies.set('token', token, { expires: 1, secure: true, sameSite: 'Strict' });
} catch (e) {
publishError(e as Error);
Expand All @@ -44,23 +46,23 @@ export function AuthProvider({ children }: AuthProviderProps) {

const loginWithGithub = () => loginWithProvider(githubAuthProvider);

const logout = () => {
const logout = async () => {
await signOut(auth);
Cookies.remove('token');
return signOut(auth);
window.location.href = '/';
};

const deleteAccount = async () => {
await currentUser?.delete();
Cookies.remove('token');
window.location.href = '/';
};

useEffect(() => {
return auth.onAuthStateChanged(user => {
setCurrentUser(user);
setLoading(false);
});
}, []);

useEffect(() => {
console.log(currentUser);
if (!currentUser && window.location.pathname !== '/') {
window.location.href = '/';
}
}, [currentUser]);

return (
Expand All @@ -70,6 +72,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
loginWithGoogle,
loginWithGithub,
logout,
deleteAccount,
}}
>
{!loading && children}
Expand Down
12 changes: 0 additions & 12 deletions code/client/src/ui/pages/home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import { Link } from 'react-router-dom';
import './Home.scss';
import { useEffect } from 'react';
import { useCommunication } from '@ui/contexts/communication/useCommunication';

function Home() {
const { http } = useCommunication();

useEffect(() => {
async function test() {
const result = await http.get('/protected', true);
console.log('Logged in', result);
}
test();
}, [http]);

return (
<div className="home">
<h2>Home</h2>
Expand Down
4 changes: 2 additions & 2 deletions code/client/src/ui/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function Profile() {
const { id } = useParams();
const { getUser, deleteUser } = useAuthService();
const [user, setUser] = useState<User | null>(null);
const { currentUser } = useAuth();
const { currentUser, deleteAccount } = useAuth();
const navigate = useNavigate();

useEffect(() => {
Expand All @@ -24,7 +24,7 @@ function Profile() {

async function onDeleteAccount() {
if (!currentUser || currentUser.uid !== user?.id) return;
await currentUser.delete();
await deleteAccount();
await deleteUser(user.id);
navigate('/');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.manage-members-dialog {
ul {
padding: 0;
margin: 0;
}

li {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
list-style: none;
}
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,46 @@
import Dialog from '@ui/components/dialog/Dialog';
import { FaCross, FaUsers } from 'react-icons/fa6';
import { UserData } from '@notespace/shared/src/users/types';
import { FaUsers } from 'react-icons/fa6';
import { RxCross1 } from 'react-icons/rx';
import './ManageMembersDialog.scss';

type ManageMembersDialog = {
members: UserData[];
members: string[];
onAddMember: (email: string) => void;
onRemoveMember: (email: string) => void;
};

function ManageMembersDialog({ members, onAddMember, onRemoveMember }: ManageMembersDialog) {
return (
<Dialog
title="Manage members in workspace"
title="Manage Members"
fields={[{ name: 'Add new member', label: 'User email' }]}
onSubmit={values => {
onAddMember(values['Add new member']);
}}
submitText="Add Member"
extraContent={
<div>
<h3>Members</h3>
<div className="manage-members-dialog">
<h4>Current Members</h4>
<ul>
{members?.map(member => (
<li key={member.email}>
<p>{member.email}</p>
<button onClick={() => onRemoveMember(member.email)}>
<FaCross />
</button>
<li key={member}>
<p>{member}</p>
{members.length > 1 && (
<button onClick={() => onRemoveMember(member)}>
<RxCross1 />
</button>
)}
</li>
))}
</ul>
<small>Add workspace member</small>
</div>
}
>
<div>
<>
<FaUsers />
Members
</div>
</>
</Dialog>
);
}
Expand Down
5 changes: 2 additions & 3 deletions code/client/src/ui/pages/workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import DataTable from '@ui/components/table/DataTable';
import { MdDelete } from 'react-icons/md';
import { useEffect, useState } from 'react';
import { sortWorkspaces } from '@domain/workspaces/utils';
import './Workspaces.scss';
import { useCommunication } from '@ui/contexts/communication/useCommunication';
import { UserData } from '@notespace/shared/src/users/types';
import './Workspaces.scss';

function Workspaces() {
const { workspaces, operations } = useWorkspaces();
Expand Down Expand Up @@ -61,7 +60,7 @@ function Workspaces() {
}
onDelete={() => operations.deleteWorkspace(workspace.id).catch(publishError)}
onRename={name => operations.updateWorkspace(workspace.id, { ...workspace, name }).catch(publishError)}
onGetMembers={() => operations.getWorkspaceMembers(workspace.id).catch(publishError) as Promise<UserData[]>}
onGetMembers={() => operations.getWorkspaceMembers(workspace.id).catch(publishError) as Promise<string[]>}
onAddMember={email => operations.addWorkspaceMember(workspace.id, email).catch(publishError)}
onRemoveMember={email => operations.removeWorkspaceMember(workspace.id, email).catch(publishError)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import PopupMenu from '@ui/components/popup-menu/PopupMenu';
import { ReactNode, useEffect, useState } from 'react';
import { MdDelete, MdEdit } from 'react-icons/md';
import ManageMembersDialog from '@ui/pages/workspace/components/ManageMembersDialog';
import { UserData } from '@notespace/shared/src/users/types';

type WorkspaceContextMenuProps = {
children: ReactNode;
onRename: () => void;
onDelete: () => void;
onGetMembers: () => Promise<UserData[]>;
onGetMembers: () => Promise<string[]>;
onAddMember: (email: string) => Promise<void>;
onRemoveMember: (email: string) => Promise<void>;
};
Expand All @@ -21,11 +20,11 @@ function WorkspaceContextMenu({
onAddMember,
onRemoveMember,
}: WorkspaceContextMenuProps) {
const [members, setMembers] = useState<UserData[]>([]);
const [members, setMembers] = useState<string[]>([]);

useEffect(() => {
onGetMembers().then(setMembers);
}, [onGetMembers]);
}, []); // eslint-disable-line react-hooks/exhaustive-deps

return (
<PopupMenu item={children}>
Expand Down
Loading

0 comments on commit b462485

Please sign in to comment.