Skip to content

Commit 0a91f63

Browse files
create-issue-branch[bot]Steve Gilroy
andauthored
3 Display auto saving icons and message if logged in (#277)
- 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]>
1 parent 43e5056 commit 0a91f63

32 files changed

+569
-452
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88
### Added
99

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

1617
## Changed
1718

19+
- Refactor API thunks and save logic (#268)
1820
- Removed file menu for `main.py` (#269)
1921
- Refactored project saving (#270), loading (#270) and remixing (#276) into redux asynchronous thunks
2022
- Creates remix if save button clicked when logged-in user does not own project (#276)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@szhsin/react-menu": "^3.2.0",
2929
"axios": "^0.24.0",
3030
"codemirror": "^6.0.1",
31+
"date-fns": "^2.29.3",
3132
"file-saver": "^2.0.5",
3233
"fs-extra": "^9.0.1",
3334
"highcharts": "^9.3.1",

src/App.js

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
/* eslint-disable react-hooks/exhaustive-deps */
22
import './App.scss';
33

4-
import { useEffect, useState } from 'react';
4+
import { useEffect } from 'react';
55
import { useCookies } from 'react-cookie';
66
import { BrowserRouter } from 'react-router-dom';
7-
import { useDispatch, useSelector } from 'react-redux';
7+
import { useSelector } from 'react-redux';
88
import { ToastContainer } from 'react-toastify';
99

1010
import { SettingsContext } from './settings';
1111
import Header from './components/Header/Header'
1212
import Routes from './components/Routes'
1313
import GlobalNav from './components/GlobalNav/GlobalNav';
1414
import Footer from './components/Footer/Footer';
15-
import { saveProject } from './components/Editor/EditorSlice';
1615
import BetaBanner from './components/BetaBanner/BetaBanner';
1716
import BetaModal from './components/Modals/BetaModal';
1817
import LoginToSaveModal from './components/Modals/LoginToSaveModal';
@@ -23,33 +22,14 @@ function App() {
2322
const [cookies] = useCookies(['theme', 'fontSize'])
2423
const themeDefault = window.matchMedia("(prefers-color-scheme:dark)").matches ? "dark" : "light"
2524

26-
const project = useSelector((state) => state.editor.project)
27-
const user = useSelector((state) => state.auth.user)
28-
const projectLoaded = useSelector((state) => state.editor.projectLoaded)
2925
const saving = useSelector((state) => state.editor.saving)
30-
const autosaved = useSelector((state) => state.editor.lastSaveAutosaved)
31-
const [timeoutId, setTimeoutId] = useState(null);
32-
33-
const dispatch = useDispatch()
34-
35-
useEffect(() => {
36-
if(timeoutId) clearTimeout(timeoutId);
37-
const id = setTimeout(async () => {
38-
if (user && project.user_id === user.profile.user && projectLoaded === 'success') {
39-
dispatch(saveProject({project: project, user: user, autosave: true}))
40-
} else if (projectLoaded === 'success') {
41-
localStorage.setItem(project.identifier || 'project', JSON.stringify(project))
42-
}
43-
}, 2000);
44-
setTimeoutId(id);
45-
46-
}, [project, user, projectLoaded, dispatch])
26+
const autosave = useSelector((state) => state.editor.lastSaveAutosave)
4727

4828
useEffect(() => {
49-
if (saving === 'success' && autosaved === false) {
29+
if (saving === 'success' && autosave === false) {
5030
showSavedMessage()
5131
}
52-
}, [saving, autosaved])
32+
}, [saving, autosave])
5333

5434
return (
5535
<div

src/App.test.js

Lines changed: 2 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { act, render, screen, waitFor } from '@testing-library/react';
55
import { Cookies, CookiesProvider } from 'react-cookie';
66
import configureStore from 'redux-mock-store';
77
import { showSavedMessage } from './utils/Notifications';
8-
import { saveProject } from './components/Editor/EditorSlice';
98

109
jest.mock('./utils/Notifications')
1110
jest.mock('./components/Editor/EditorSlice', () => {
@@ -137,6 +136,7 @@ describe("When selecting the font size", ()=>{
137136
}
138137
store = mockStore(initialState);
139138
})
139+
140140
test("Cookie not set defaults css class to small", () => {
141141
const appContainer = render(
142142
<CookiesProvider cookies={cookies}>
@@ -236,7 +236,7 @@ test('Successful manual save prompts project saved message', async () => {
236236
const initialState = {
237237
editor: {
238238
saving: 'success',
239-
lastSaveAutosaved: false
239+
lastSaveAutosave: false
240240
},
241241
auth: {}
242242
}
@@ -246,133 +246,3 @@ test('Successful manual save prompts project saved message', async () => {
246246
})
247247

248248
// TODO: Write test for successful autosave not prompting the project saved message as per the above
249-
250-
describe('When not logged in', () => {
251-
const project = {
252-
name: 'hello world',
253-
project_type: 'python',
254-
identifier: 'hello-world-project',
255-
components: [
256-
{
257-
name: 'main',
258-
extension: 'py',
259-
content: '# hello'
260-
}
261-
]
262-
}
263-
beforeEach(() => {
264-
const middlewares = []
265-
const mockStore = configureStore(middlewares)
266-
const initialState = {
267-
editor: {
268-
project: project,
269-
projectLoaded: 'success'
270-
},
271-
auth: {
272-
user: null
273-
}
274-
}
275-
const mockedStore = mockStore(initialState);
276-
render(<Provider store={mockedStore}><App/></Provider>);
277-
})
278-
279-
afterEach(() => {
280-
localStorage.clear()
281-
})
282-
283-
test('Project saved in localStorage', async () => {
284-
await waitFor(() => expect(localStorage.getItem('hello-world-project')).toEqual(JSON.stringify(project)), {timeout: 2100})
285-
})
286-
})
287-
288-
describe('When logged in and user does not own project', () => {
289-
const project = {
290-
name: 'hello world',
291-
project_type: 'python',
292-
identifier: 'hello-world-project',
293-
components: [
294-
{
295-
name: 'main',
296-
extension: 'py',
297-
content: '# hello'
298-
}
299-
],
300-
user_id: 'another_user'
301-
}
302-
const user = {
303-
profile: {
304-
user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf"
305-
}
306-
}
307-
308-
beforeEach(() => {
309-
const middlewares = []
310-
const mockStore = configureStore(middlewares)
311-
const initialState = {
312-
editor: {
313-
project,
314-
projectLoaded: 'success'
315-
},
316-
auth: {user}
317-
}
318-
const mockedStore = mockStore(initialState);
319-
render(<Provider store={mockedStore}><App/></Provider>);
320-
})
321-
322-
afterEach(() => {
323-
localStorage.clear()
324-
})
325-
326-
test('Project saved in localStorage', async () => {
327-
await waitFor(() => expect(localStorage.getItem('hello-world-project')).toEqual(JSON.stringify(project)), {timeout: 2100})
328-
})
329-
330-
})
331-
332-
describe('When logged in and user owns project', () => {
333-
const project = {
334-
name: 'hello world',
335-
project_type: 'python',
336-
identifier: 'hello-world-project',
337-
components: [
338-
{
339-
name: 'main',
340-
extension: 'py',
341-
content: '# hello'
342-
}
343-
],
344-
user_id: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf"
345-
}
346-
const user = {
347-
profile: {
348-
user: "b48e70e2-d9ed-4a59-aee5-fc7cf09dbfaf"
349-
}
350-
}
351-
352-
let mockedStore;
353-
354-
beforeEach(() => {
355-
const middlewares = []
356-
const mockStore = configureStore(middlewares)
357-
const initialState = {
358-
editor: {
359-
project,
360-
projectLoaded: 'success'
361-
},
362-
auth: {user}
363-
}
364-
mockedStore = mockStore(initialState);
365-
render(<Provider store={mockedStore}><App/></Provider>);
366-
})
367-
368-
afterEach(() => {
369-
localStorage.clear()
370-
})
371-
372-
test('Project autosaved to database', async () => {
373-
const saveAction = {type: 'SAVE_PROJECT' }
374-
saveProject.mockImplementationOnce(() => (saveAction))
375-
await waitFor(() => expect(saveProject).toHaveBeenCalledWith({project, user, autosave: true}), {timeout: 2100})
376-
expect(mockedStore.getActions()[1]).toEqual(saveAction)
377-
})
378-
})

src/Icons.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,25 @@ export const TickIcon = () => {
217217
</svg>
218218
)
219219
}
220+
221+
export const CloudUploadIcon = () => {
222+
const [cookies] = useCookies(['fontSize'])
223+
const scale = fontScaleFactors[cookies.fontSize] || 1
224+
return (
225+
<svg transform={`scale(${scale}, ${scale})`} width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
226+
<path d="M14.0139 19.6065V15.5456H17.058L12.9992 10.4695L8.94049 15.5456H11.9846V19.6065H14.0139Z" />
227+
<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" />
228+
</svg>
229+
)
230+
}
231+
232+
export const CloudTickIcon = () => {
233+
const [cookies] = useCookies(['fontSize'])
234+
const scale = fontScaleFactors[cookies.fontSize] || 1
235+
return (
236+
<svg transform={`scale(${scale}, ${scale})`} width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
237+
<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"/>
238+
<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"/>
239+
</svg>
240+
)
241+
}

src/app/store.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const store = configureStore({
1111
middleware: (getDefaultMiddleware) =>
1212
getDefaultMiddleware({
1313
serializableCheck: {
14-
ignoredActions: ['redux-oidc/USER_FOUND'],
14+
ignoredActions: ['redux-oidc/USER_FOUND', 'redux-odic/SILENT_RENEW_ERROR'],
1515
ignoredPaths: ['auth.user'],
1616
},
1717
}),

0 commit comments

Comments
 (0)