Skip to content

Commit

Permalink
Use new GraphQL API for all API requests on project index page (#376)
Browse files Browse the repository at this point in the history
## What's changed?

* Adds apollo-client, graphql packages
* Removes useProjectList hook]
* Fetches ProjectIndex data using GraphQL API, with fragments for
pagination, project list item etc.
* Uses GraphQL API for DeleteProject and RenameProject modals

## Points for consideration

* When renaming a project the list doesn't get re-sorted by "updated at"
time
* We're sorta changing state without telling Redux

## Before deployment


[editor-api#139](RaspberryPiFoundation/editor-api#139)
needs merging

---------

Co-authored-by: Izzy Smillie <[email protected]>
  • Loading branch information
patch0 and IzzySmillie authored Mar 2, 2023
1 parent 59902c2 commit 2a0e2f5
Show file tree
Hide file tree
Showing 20 changed files with 687 additions and 386 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ build/
dist/
.git/
node_modules/
.yarn/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Renaming project, adding new file or renaming file triggers autosave immediately (#368)
- Bump http-cache-semantics from 4.1.0 to 4.1.1 (#361)
- Removed redundant file indices (#377)
- Use GraphQL API to fetch project index page (#376)

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.12.0",
"private": true,
"dependencies": {
"@apollo/client": "^3.7.8",
"@babel/core": "^7.17.10",
"@codemirror/commands": "^6.1.1",
"@codemirror/lang-css": "^6.0.0",
Expand Down Expand Up @@ -31,6 +32,7 @@
"date-fns": "^2.29.3",
"file-saver": "^2.0.5",
"fs-extra": "^9.0.1",
"graphql": "^16.6.0",
"highcharts": "^9.3.1",
"highcharts-react-official": "^3.1.0",
"i18next": "^22.0.3",
Expand Down
17 changes: 0 additions & 17 deletions src/components/Editor/Hooks/useProjectList.js

This file was deleted.

76 changes: 0 additions & 76 deletions src/components/Editor/Hooks/useProjectList.test.js

This file was deleted.

21 changes: 17 additions & 4 deletions src/components/Modals/DeleteProjectModal.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import React from "react";
import Modal from 'react-modal';
import { gql, useMutation } from '@apollo/client';
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { closeDeleteProjectModal, syncProject } from "../Editor/EditorSlice";
import { closeDeleteProjectModal } from "../Editor/EditorSlice";
import { CloseIcon } from "../../Icons";
import Button from "../Button/Button";

const DeleteProjectModal = () => {
// Define mutation
export const DELETE_PROJECT_MUTATION = gql`
mutation DeleteProject($id: String!) {
deleteProject(input: {id: $id}) {
id
}
}
`;

export const DeleteProjectModal = () => {
const dispatch = useDispatch()
const { t } = useTranslation();
const isModalOpen = useSelector((state) => state.editor.deleteProjectModalShowing)
const project = useSelector((state) => state.editor.modals.deleteProject)
const user = useSelector((state) => state.auth.user)

const closeModal = () => dispatch(closeDeleteProjectModal());

// This can capture data, error, loading as per normal queries, but we're not
// using them yet.
const [deleteProjectMutation] = useMutation(DELETE_PROJECT_MUTATION, {refetchQueries: ["ProjectIndexQuery"]})

const onClickDelete = async () => {
dispatch(syncProject('delete')({identifier: project.identifier, accessToken: user.access_token}));
deleteProjectMutation({variables: {id: project.id}, onCompleted: closeModal})
}

return (
Expand Down
64 changes: 43 additions & 21 deletions src/components/Modals/DeleteProjectModal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@ import React from "react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { MockedProvider } from "@apollo/client/testing";

import DeleteProjectModal from "./DeleteProjectModal";
import { syncProject } from "../Editor/EditorSlice";

jest.mock('../Editor/EditorSlice', () => ({
...jest.requireActual('../Editor/EditorSlice'),
syncProject: jest.fn((_) => jest.fn())
}))
import { DeleteProjectModal, DELETE_PROJECT_MUTATION } from "./DeleteProjectModal";

describe("Testing the delete project modal", () => {
let store;
let deleteButton;
let user = { access_token: 'my_access_token' }
let project = { identifier: 'project-to-delete', name: 'my first project' }
let mocks;
let project = { id: 'abc', name: 'my first project' }

beforeEach(() => {
const middlewares = []
Expand All @@ -26,12 +20,36 @@ describe("Testing the delete project modal", () => {
deleteProject: project
},
deleteProjectModalShowing: true
},
auth: {user}
}
}

mocks = [
{
request: {
query: DELETE_PROJECT_MUTATION,
variables: { id: project.id }
},
result: jest.fn(() => ({
data: {
deleteProject: {
id: project.id
}
}
}))
}
]

store = mockStore(initialState);
render(<Provider store={store}><div id='app'><DeleteProjectModal currentName='main' currentExtension='py' fileKey={0} /></div></Provider>)
deleteButton = screen.getByText('projectList.deleteProjectModal.delete')

render(
<MockedProvider mocks={mocks}>
<Provider store={store}>
<div id='app'>
<DeleteProjectModal />
</div>
</Provider>
</MockedProvider>
)
})

test('Modal renders', () => {
Expand All @@ -50,12 +68,16 @@ describe("Testing the delete project modal", () => {
expect(store.getActions()).toEqual([{type: 'editor/closeDeleteProjectModal'}])
})

test("Clicking delete button deletes the project", async () => {
const deleteAction = {type: 'DELETE_PROJECT' }
const deleteProject = jest.fn(() => deleteAction)
syncProject.mockImplementationOnce(jest.fn((_) => (deleteProject)))
fireEvent.click(deleteButton)
await waitFor(() => expect(deleteProject).toHaveBeenCalledWith({ identifier: 'project-to-delete', accessToken: user.access_token }))
expect(store.getActions()[0]).toEqual(deleteAction)
test("Clicking delete button (eventually) closes the modal", async () => {
const deleteButton = screen.getByText('projectList.deleteProjectModal.delete')
fireEvent.click(deleteButton)
await waitFor(() => expect(store.getActions()).toEqual([{type: 'editor/closeDeleteProjectModal'}]))
})

test("Clicking delete button calls the mutation", async () => {
const deleteButton = screen.getByText('projectList.deleteProjectModal.delete')
const deleteProjectMutationMock = mocks[0].result
fireEvent.click(deleteButton)
await waitFor(() => expect(deleteProjectMutationMock).toHaveBeenCalled())
})
})
31 changes: 26 additions & 5 deletions src/components/Modals/RenameProjectModal.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
import React from "react";
import Modal from 'react-modal';
import { gql, useMutation } from '@apollo/client';
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { closeRenameProjectModal, syncProject } from "../Editor/EditorSlice";
import { closeRenameProjectModal } from "../Editor/EditorSlice";
import { showRenamedMessage } from '../../utils/Notifications';
import { CloseIcon } from "../../Icons";
import Button from "../Button/Button";

const RenameProjectModal = () => {
export const RENAME_PROJECT_MUTATION = gql`
mutation RenameProject($id: String!, $name: String!) {
updateProject(input: {id: $id, name: $name}) {
project {
id
name
updatedAt
}
}
}
`;

export const RenameProjectModal = () => {
const dispatch = useDispatch()
const { t } = useTranslation();
const isModalOpen = useSelector((state) => state.editor.renameProjectModalShowing)
const project = useSelector((state) => state.editor.modals.renameProject)
const user = useSelector((state) => state.auth.user)
const closeModal = () => dispatch(closeRenameProjectModal())

const onCompleted = () => {
closeModal()
showRenamedMessage()
}

const closeModal = () => dispatch(closeRenameProjectModal());
// This can capture data, error, loading as per normal queries, but we're not
// using them yet.
const [renameProjectMutation] = useMutation(RENAME_PROJECT_MUTATION);

const renameProject = () => {
const newName = document.getElementById('name').value
dispatch(syncProject('save')({project: {...project, name: newName}, accessToken: user.access_token, autosave: false}))
renameProjectMutation({variables: {id: project.id, name: newName}, onCompleted: onCompleted})
}

return (
Expand Down
Loading

0 comments on commit 2a0e2f5

Please sign in to comment.