Skip to content

Commit

Permalink
3 Display auto saving icons and message if logged in (#277)
Browse files Browse the repository at this point in the history
- Added icons for saving and saved to header
- Translations for saving
- Add date logic (locale is English for date words currently)
- Refactored async thunks for project load/save/remix API
- Tweaked save logic after updates/typing
- Added control logic to reduce extra save/loads when project state
isn't changed
- Save triggers in Project not App

Co-authored-by: Steve Gilroy <[email protected]>
  • Loading branch information
create-issue-branch[bot] and Steve Gilroy authored Dec 12, 2022
1 parent 43e5056 commit 0a91f63
Show file tree
Hide file tree
Showing 32 changed files with 569 additions and 452 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Beta banner and modal (#266)
- Autosave icons and status (#268)
- Autosave project to database if user logged in and owns project (#270)
- Autosave project changes to local storage if user not logged in or does not own project (#270)
- Modal to prompt login or download if save button clicked when not logged in (#276)
- Ability to rename any project (#284)

## Changed

- Refactor API thunks and save logic (#268)
- Removed file menu for `main.py` (#269)
- Refactored project saving (#270), loading (#270) and remixing (#276) into redux asynchronous thunks
- Creates remix if save button clicked when logged-in user does not own project (#276)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@szhsin/react-menu": "^3.2.0",
"axios": "^0.24.0",
"codemirror": "^6.0.1",
"date-fns": "^2.29.3",
"file-saver": "^2.0.5",
"fs-extra": "^9.0.1",
"highcharts": "^9.3.1",
Expand Down
30 changes: 5 additions & 25 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/* eslint-disable react-hooks/exhaustive-deps */
import './App.scss';

import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { useCookies } from 'react-cookie';
import { BrowserRouter } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { ToastContainer } from 'react-toastify';

import { SettingsContext } from './settings';
import Header from './components/Header/Header'
import Routes from './components/Routes'
import GlobalNav from './components/GlobalNav/GlobalNav';
import Footer from './components/Footer/Footer';
import { saveProject } from './components/Editor/EditorSlice';
import BetaBanner from './components/BetaBanner/BetaBanner';
import BetaModal from './components/Modals/BetaModal';
import LoginToSaveModal from './components/Modals/LoginToSaveModal';
Expand All @@ -23,33 +22,14 @@ function App() {
const [cookies] = useCookies(['theme', 'fontSize'])
const themeDefault = window.matchMedia("(prefers-color-scheme:dark)").matches ? "dark" : "light"

const project = useSelector((state) => state.editor.project)
const user = useSelector((state) => state.auth.user)
const projectLoaded = useSelector((state) => state.editor.projectLoaded)
const saving = useSelector((state) => state.editor.saving)
const autosaved = useSelector((state) => state.editor.lastSaveAutosaved)
const [timeoutId, setTimeoutId] = useState(null);

const dispatch = useDispatch()

useEffect(() => {
if(timeoutId) clearTimeout(timeoutId);
const id = setTimeout(async () => {
if (user && project.user_id === user.profile.user && projectLoaded === 'success') {
dispatch(saveProject({project: project, user: user, autosave: true}))
} else if (projectLoaded === 'success') {
localStorage.setItem(project.identifier || 'project', JSON.stringify(project))
}
}, 2000);
setTimeoutId(id);

}, [project, user, projectLoaded, dispatch])
const autosave = useSelector((state) => state.editor.lastSaveAutosave)

useEffect(() => {
if (saving === 'success' && autosaved === false) {
if (saving === 'success' && autosave === false) {
showSavedMessage()
}
}, [saving, autosaved])
}, [saving, autosave])

return (
<div
Expand Down
134 changes: 2 additions & 132 deletions src/App.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { act, render, screen, waitFor } from '@testing-library/react';
import { Cookies, CookiesProvider } from 'react-cookie';
import configureStore from 'redux-mock-store';
import { showSavedMessage } from './utils/Notifications';
import { saveProject } from './components/Editor/EditorSlice';

jest.mock('./utils/Notifications')
jest.mock('./components/Editor/EditorSlice', () => {
Expand Down Expand Up @@ -137,6 +136,7 @@ describe("When selecting the font size", ()=>{
}
store = mockStore(initialState);
})

test("Cookie not set defaults css class to small", () => {
const appContainer = render(
<CookiesProvider cookies={cookies}>
Expand Down Expand Up @@ -236,7 +236,7 @@ test('Successful manual save prompts project saved message', async () => {
const initialState = {
editor: {
saving: 'success',
lastSaveAutosaved: false
lastSaveAutosave: false
},
auth: {}
}
Expand All @@ -246,133 +246,3 @@ test('Successful manual save prompts project saved message', async () => {
})

// TODO: Write test for successful autosave not prompting the project saved message as per the above

describe('When not logged in', () => {
const project = {
name: 'hello world',
project_type: 'python',
identifier: 'hello-world-project',
components: [
{
name: 'main',
extension: 'py',
content: '# hello'
}
]
}
beforeEach(() => {
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {
editor: {
project: project,
projectLoaded: 'success'
},
auth: {
user: null
}
}
const mockedStore = mockStore(initialState);
render(<Provider store={mockedStore}><App/></Provider>);
})

afterEach(() => {
localStorage.clear()
})

test('Project saved in localStorage', async () => {
await waitFor(() => expect(localStorage.getItem('hello-world-project')).toEqual(JSON.stringify(project)), {timeout: 2100})
})
})

describe('When logged in and user does not own project', () => {
const project = {
name: 'hello world',
project_type: 'python',
identifier: 'hello-world-project',
components: [
{
name: 'main',
extension: 'py',
content: '# hello'
}
],
user_id: 'another_user'
}
const user = {
profile: {
user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf"
}
}

beforeEach(() => {
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {
editor: {
project,
projectLoaded: 'success'
},
auth: {user}
}
const mockedStore = mockStore(initialState);
render(<Provider store={mockedStore}><App/></Provider>);
})

afterEach(() => {
localStorage.clear()
})

test('Project saved in localStorage', async () => {
await waitFor(() => expect(localStorage.getItem('hello-world-project')).toEqual(JSON.stringify(project)), {timeout: 2100})
})

})

describe('When logged in and user owns project', () => {
const project = {
name: 'hello world',
project_type: 'python',
identifier: 'hello-world-project',
components: [
{
name: 'main',
extension: 'py',
content: '# hello'
}
],
user_id: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf"
}
const user = {
profile: {
user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf"
}
}

let mockedStore;

beforeEach(() => {
const middlewares = []
const mockStore = configureStore(middlewares)
const initialState = {
editor: {
project,
projectLoaded: 'success'
},
auth: {user}
}
mockedStore = mockStore(initialState);
render(<Provider store={mockedStore}><App/></Provider>);
})

afterEach(() => {
localStorage.clear()
})

test('Project autosaved to database', async () => {
const saveAction = {type: 'SAVE_PROJECT' }
saveProject.mockImplementationOnce(() => (saveAction))
await waitFor(() => expect(saveProject).toHaveBeenCalledWith({project, user, autosave: true}), {timeout: 2100})
expect(mockedStore.getActions()[1]).toEqual(saveAction)
})
})
22 changes: 22 additions & 0 deletions src/Icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,25 @@ export const TickIcon = () => {
</svg>
)
}

export const CloudUploadIcon = () => {
const [cookies] = useCookies(['fontSize'])
const scale = fontScaleFactors[cookies.fontSize] || 1
return (
<svg transform={`scale(${scale}, ${scale})`} width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0139 19.6065V15.5456H17.058L12.9992 10.4695L8.94049 15.5456H11.9846V19.6065H14.0139Z" />
<path d="M7.77966 19.6064H9.80904V17.5759H7.77966C6.10136 17.5759 4.73559 16.2095 4.73559 14.5303C4.73559 13.1049 5.95221 11.7323 7.44786 11.4694L8.03739 11.3658L8.23221 10.7994C8.94554 8.71714 10.7172 7.42375 12.8531 7.42375C15.6506 7.42375 17.9266 9.70089 17.9266 12.4998V13.5151H18.9412C20.0604 13.5151 20.9706 14.4257 20.9706 15.5455C20.9706 16.6653 20.0604 17.5759 18.9412 17.5759H15.8972V19.6064H18.9412C21.1796 19.6064 23 17.7851 23 15.5455C22.9984 14.6355 22.6922 13.7522 22.1301 13.0367C21.568 12.3213 20.7826 11.8148 19.8991 11.5983C19.4557 8.10395 16.4654 5.39331 12.8531 5.39331C10.0566 5.39331 7.62746 7.02883 6.5184 9.60647C4.33885 10.2582 2.70621 12.3171 2.70621 14.5303C2.70621 17.3292 4.98216 19.6064 7.77966 19.6064Z" />
</svg>
)
}

export const CloudTickIcon = () => {
const [cookies] = useCookies(['fontSize'])
const scale = fontScaleFactors[cookies.fontSize] || 1
return (
<svg transform={`scale(${scale}, ${scale})`} width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.77953 19.6065H9.80893V17.5761H7.77953C6.10122 17.5761 4.73544 16.2096 4.73544 14.5304C4.73544 13.105 5.95206 11.7324 7.44772 11.4695L8.03726 11.3659L8.23209 10.7994C8.94542 8.71717 10.7171 7.42377 12.853 7.42377C15.6505 7.42377 17.9265 9.70093 17.9265 12.4999V13.5151H18.9412C20.0604 13.5151 20.9706 14.4258 20.9706 15.5456C20.9706 16.6654 20.0604 17.5761 18.9412 17.5761H15.8971V19.6065H18.9412C21.1796 19.6065 23 17.7852 23 15.5456C22.9984 14.6356 22.6922 13.7523 22.1301 13.0368C21.568 12.3213 20.7825 11.8149 19.8991 11.5984C19.4557 8.10397 16.4653 5.39331 12.853 5.39331C10.0565 5.39331 7.62733 7.02884 6.51826 9.60651C4.33869 10.2583 2.70604 12.3172 2.70604 14.5304C2.70604 17.3294 4.98201 19.6065 7.77953 19.6065Z"/>
<path fillRule="evenodd" clipRule="evenodd" d="M16.1836 11.8558L11.6817 16.7791L9.17648 14.4342L10.3575 13.1724L11.5862 14.3224L14.9081 10.6895L16.1836 11.8558Z"/>
</svg>
)
}
2 changes: 1 addition & 1 deletion src/app/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const store = configureStore({
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['redux-oidc/USER_FOUND'],
ignoredActions: ['redux-oidc/USER_FOUND', 'redux-odic/SILENT_RENEW_ERROR'],
ignoredPaths: ['auth.user'],
},
}),
Expand Down
Loading

0 comments on commit 0a91f63

Please sign in to comment.