Skip to content

Commit

Permalink
Merge pull request #70 from Gkuzin13/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Gkuzin13 authored Jan 2, 2024
2 parents 58d9313 + af40ff6 commit 80edf87
Show file tree
Hide file tree
Showing 80 changed files with 1,899 additions and 1,396 deletions.
1 change: 1 addition & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"eslint-plugin-playwright": "^0.15.3",
"happy-dom": "^10.9.0",
"jsdom": "^22.1.0",
"msw": "*",
"resize-observer-polyfill": "^1.5.1",
"vite-plugin-pwa": "^0.16.4",
"vitest-canvas-mock": "^0.3.2"
Expand Down
166 changes: 62 additions & 104 deletions apps/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,134 +1,92 @@
import { useEffect, useLayoutEffect } from 'react';
import { useEffect } from 'react';
import {
LOCAL_STORAGE_KEY,
appState,
libraryState,
PAGE_URL_SEARCH_PARAM_KEY,
LOCAL_STORAGE_LIBRARY_KEY,
PAGE_URL_SEARCH_PARAM_KEY,
} from '@/constants/app';
import { useAppDispatch, useAppStore } from '@/stores/hooks';
import { canvasActions } from '@/stores/slices/canvas';
import { useAppDispatch } from '@/stores/hooks';
import { canvasActions } from '@/services/canvas/slice';
import { storage } from '@/utils/storage';
import MainLayout from './components/Layout/MainLayout/MainLayout';
import { useWebSocket } from './contexts/websocket';
import useWSMessage from './hooks/useWSMessage';
import useUrlSearchParams from './hooks/useUrlSearchParams/useUrlSearchParams';
import { historyActions } from './stores/reducers/history';
import { collaborationActions } from './stores/slices/collaboration';
import { libraryActions } from './stores/slices/library';
import { libraryActions } from '@/services/library/slice';
import {
addCollabActionsListeners,
subscribeToIncomingCollabMessages,
} from './services/collaboration/listeners';
import useParam from './hooks/useParam/useParam';
import api from './services/api';
import { useNotifications } from './contexts/notifications';
import type { Library, AppState } from '@/constants/app';

const App = () => {
const params = useUrlSearchParams();
const roomId = useParam(PAGE_URL_SEARCH_PARAM_KEY);

const ws = useWebSocket();
const store = useAppStore();
const { addNotification } = useNotifications();

const dispatch = useAppDispatch();

useWSMessage(ws.connection, (message) => {
const { type, data } = message;
useEffect(() => {
if (!roomId) return;

switch (type) {
case 'user-joined': {
dispatch(collaborationActions.addUser(data));
break;
}
case 'user-change': {
dispatch(collaborationActions.updateUser(data));
break;
}
case 'user-left': {
dispatch(collaborationActions.removeUser(data));
break;
}
case 'nodes-set': {
dispatch(canvasActions.setNodes(data));
break;
}
case 'nodes-add': {
dispatch(canvasActions.addNodes(data));
break;
}
case 'draft-finish': {
dispatch(canvasActions.addNodes([data.node]));
break;
}
case 'nodes-update': {
dispatch(canvasActions.updateNodes(data));
break;
}
case 'nodes-delete': {
dispatch(canvasActions.deleteNodes(data));
break;
}
case 'nodes-move-to-start': {
dispatch(canvasActions.moveNodesToStart(data));
break;
}
case 'nodes-move-to-end': {
dispatch(canvasActions.moveNodesToEnd(data));
break;
}
case 'nodes-move-forward': {
dispatch(canvasActions.moveNodesForward(data));
break;
}
case 'nodes-move-backward': {
dispatch(canvasActions.moveNodesBackward(data));
break;
}
case 'draft-text-update': {
const textNode = store
.getState()
.canvas.present.nodes.find(
(node) => node.nodeProps.id === data.nodeId,
);

if (textNode) {
dispatch(
canvasActions.updateNodes([{ ...textNode, text: data.text }]),
);
}
break;
}
case 'history-change': {
const action = historyActions[data.action];
dispatch(action());
const [request, abortController] = api.getPage(roomId);

request
.then(({ page }) => {
dispatch(canvasActions.setNodes(page.nodes, { broadcast: false }));
addNotification({
title: 'Live collaboration',
description: 'You are connected',
type: 'success',
});
})
.catch(() => {
addNotification({
title: 'Live collaboration',
description: 'Disconnected',
type: 'info',
});
});

return () => {
if (abortController.signal) {
abortController.abort();
}
};
}, [roomId, dispatch, addNotification]);

useEffect(() => {
if (!ws.isConnected || !roomId) {
return;
}
});

useLayoutEffect(() => {
const pageId = params[PAGE_URL_SEARCH_PARAM_KEY];
const subscribers = subscribeToIncomingCollabMessages(ws, dispatch);
const unsubscribe = dispatch(addCollabActionsListeners(ws, roomId));

if (ws.isConnected || pageId) {
return () => {
subscribers.forEach((unsubscribe) => unsubscribe());
unsubscribe({ cancelActive: true });
};
}, [ws, roomId, dispatch]);

useEffect(() => {
if (ws.isConnected || ws.isConnecting || roomId) {
return;
}

const state = storage.get<AppState>(LOCAL_STORAGE_KEY);
const storedCanvasState = storage.get<AppState>(LOCAL_STORAGE_KEY);

if (state) {
try {
appState.parse(state);
} catch (error) {
return;
}

dispatch(canvasActions.set(state.page));
if (storedCanvasState) {
dispatch(canvasActions.set(storedCanvasState.page));
}
}, [ws, params, dispatch]);
}, [ws, roomId, dispatch]);

useEffect(() => {
const libraryFromStorage = storage.get<Library>(LOCAL_STORAGE_LIBRARY_KEY);

if (libraryFromStorage) {
try {
libraryState.parse(libraryFromStorage);
} catch (error) {
return;
}
const storedLibrary = storage.get<Library>(LOCAL_STORAGE_LIBRARY_KEY);

dispatch(libraryActions.init(libraryFromStorage));
if (storedLibrary) {
dispatch(libraryActions.set(storedLibrary));
}
}, [dispatch]);

Expand Down
44 changes: 44 additions & 0 deletions apps/client/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
import { renderWithProviders } from '@/test/test-utils';
import App from '@/App';
import { mockGetPageResponse } from '@/test/mocks/handlers';
import { screen, waitFor } from '@testing-library/react';
import { PAGE_URL_SEARCH_PARAM_KEY } from '@/constants/app';
import { setSearchParam } from '@/test/browser-mocks';
import { nodesGenerator, stateGenerator } from '@/test/data-generators';

describe('App', () => {
afterEach(() => {
Object.defineProperty(window, 'location', window.location);
});

it('mounts without crashing', () => {
const { container } = renderWithProviders(<App />);

expect(container).toBeInTheDocument();
});

it('sets canvas state from fetched data when in collab mode', async () => {
setSearchParam(PAGE_URL_SEARCH_PARAM_KEY, mockGetPageResponse.page.id);

const { store } = renderWithProviders(<App />);

await waitFor(() => {
const { nodes } = store.getState().canvas.present;

expect(nodes).toEqual(mockGetPageResponse.page.nodes);
});
});

it('calls share this page', async () => {
Object.defineProperty(window.location, 'reload', {
value: vi.fn(),
});

const { user } = renderWithProviders(<App />, {
preloadedState: stateGenerator({
canvas: {
present: {
nodes: nodesGenerator(6),
},
},
}),
});

await user.click(screen.getByText(/Share/i));
await user.click(screen.getByText(/Share this page/i));

await waitFor(() => {
expect(screen.getByTestId(/loader/i)).toBeInTheDocument();
});
});
});
2 changes: 1 addition & 1 deletion apps/client/src/__tests__/shortcuts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@/test/test-utils';
import { TOOLS } from '@/constants/panels/tools';
import { nodesGenerator, stateGenerator } from '@/test/data-generators';
import { canvasActions } from '@/stores/slices/canvas';
import { canvasActions } from '@/services/canvas/slice';
import { mapNodesIds } from '@/utils/node';
import { historyActions } from '@/stores/reducers/history';

Expand Down
Loading

0 comments on commit 80edf87

Please sign in to comment.