From bd79c4f6f93fbcfbe3b1bd59808e01894f23020c Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:09:40 +0000 Subject: [PATCH] style: format code with Prettier and StandardJS This commit fixes the style issues introduced in 9cbf9ba according to the output from Prettier and StandardJS. Details: None --- app/public/index.html | 2 +- app/src/App.jsx | 94 ++++---- app/src/App.test.js | 186 +++++++-------- app/src/Backend/Base/Cache.js | 172 +++++++------- app/src/Backend/Base/Common.js | 50 ++-- app/src/Backend/Base/Common.test.js | 187 +++++++-------- app/src/Backend/Base/ContextCreator.jsx | 13 +- app/src/Backend/Base/GHPages.js | 102 ++++---- app/src/Backend/Base/GHPages.test.js | 222 +++++++++--------- app/src/Backend/Base/withHandlerGenerator.jsx | 12 +- .../Base/withHandlerGenerator.test.jsx | 143 ++++++----- app/src/Backend/Bcn/handler.js | 98 ++++---- app/src/Backend/Charts/handler.js | 132 +++++------ app/src/Backend/IndexesHandler.jsx | 138 +++++------ app/src/Backend/Maps/handler.js | 40 ++-- app/src/Backend/Maps/index.test.js | 10 +- app/src/Dashboard.jsx | 13 +- app/src/ErrorCatcher.jsx | 132 ++++++----- app/src/ModalRouter.jsx | 134 +++++------ app/src/ModalRouterWithRoutes.jsx | 5 +- app/src/Throtle.js | 22 +- app/src/Widget/Actions.jsx | 82 +++---- app/src/Widget/List.jsx | 172 +++++++------- app/src/Widget/MenuAddWidget.jsx | 58 ++--- app/src/Widget/MenuItem.jsx | 26 +- .../StorageContextProviderLocalStorage.jsx | 64 ++--- .../Storage/StorageContextProviderRouter.jsx | 83 ++++--- app/src/Widget/Storage/withStorageHandler.jsx | 30 +-- app/src/Widget/Widgets/Bcn/EditDataset.jsx | 54 ++--- app/src/Widget/Widgets/Bcn/Widget.jsx | 134 +++++------ app/src/Widget/Widgets/Chart/Edit.jsx | 149 ++++++------ .../Widgets/Chart/EditRegion/EditRegion.jsx | 34 +-- .../Chart/EditRegion/RecursiveTreeView.jsx | 36 +-- .../Widgets/Chart/EditRegion/RenderTree.jsx | 32 +-- .../Widget/Widgets/Chart/EditRegion/index.js | 4 +- app/src/Widget/Widgets/Chart/Widget.jsx | 172 +++++++------- app/src/Widget/Widgets/Common/Chart.jsx | 4 +- .../Widget/Widgets/Common/ChartTooltip.jsx | 14 +- .../Widget/Widgets/Common/FormDecorators.jsx | 8 +- app/src/Widget/Widgets/Common/Selector.jsx | 2 +- app/src/Widget/Widgets/Map/Edit.jsx | 106 +++++---- app/src/Widget/Widgets/Map/Legend.jsx | 16 +- app/src/Widget/Widgets/Map/Map.css | 5 +- app/src/Widget/Widgets/Map/MapImage.jsx | 200 ++++++++-------- app/src/Widget/Widgets/Map/Widget.jsx | 106 ++++----- app/src/asyncComponent.jsx | 12 +- app/src/i18n/available.test.js | 56 ++--- app/src/testHelpers.js | 104 ++++---- app/src/tests/App.int.test.js | 86 +++---- 49 files changed, 1893 insertions(+), 1863 deletions(-) diff --git a/app/public/index.html b/app/public/index.html index 6c60c6e6..8b615007 100644 --- a/app/public/index.html +++ b/app/public/index.html @@ -1,4 +1,4 @@ - + diff --git a/app/src/App.jsx b/app/src/App.jsx index b457dd89..5e3f0350 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -1,28 +1,28 @@ -import React from "react"; -import { HashRouter as Router } from "react-router-dom"; +import React from 'react' +import { HashRouter as Router } from 'react-router-dom' -import Storage from "react-simple-storage"; -import { TranslatorProvider, useTranslate } from "react-translate"; -import { Helmet, HelmetProvider } from "react-helmet-async"; -import available from "./i18n/available"; +import Storage from 'react-simple-storage' +import { TranslatorProvider, useTranslate } from 'react-translate' +import { Helmet, HelmetProvider } from 'react-helmet-async' +import available from './i18n/available' -import ErrorCatcher from "./ErrorCatcher"; -import Dashboard from "./Dashboard"; -import { WidgetsList } from "./Widget"; +import ErrorCatcher from './ErrorCatcher' +import Dashboard from './Dashboard' +import { WidgetsList } from './Widget' -import { BackendProvider, IndexesHandler } from "./Backend"; +import { BackendProvider, IndexesHandler } from './Backend' // App Helmet: Controls HTML elements with SideEffect // - Set a default title and title template, translated const AppHelmet = ({ language }) => { - const t = useTranslate("App"); - const title = t("Covid Data - Refactored"); + const t = useTranslate('App') + const title = t('Covid Data - Refactored') return ( - ); -}; + ) +} // Concentrate all providers (4) used in the app into a single component const AppProviders = ({ translations, language, children }) => ( @@ -31,79 +31,79 @@ const AppProviders = ({ translations, language, children }) => ( -
+
{children}
-); +) const fixLocationHash = () => { - const decoded = decodeURIComponent(global.location.hash); - if (decoded !== "" && decoded !== global.location.hash) { - const hash = decoded.replace(/[^#]*(#.*)$/, "$1"); - global.location.replace(hash); + const decoded = decodeURIComponent(global.location.hash) + if (decoded !== '' && decoded !== global.location.hash) { + const hash = decoded.replace(/[^#]*(#.*)$/, '$1') + global.location.replace(hash) } -}; +} const getDefaultLanguage = (available) => { - const languageNav = (global.navigator.language ?? "").toLowerCase(); + const languageNav = (global.navigator.language ?? '').toLowerCase() return available.find((language) => language.key === languageNav) ? languageNav - : "ca-es"; -}; + : 'ca-es' +} class App extends React.Component { - constructor() { - super(); + constructor () { + super() // Fix bad browser encoding HASH - fixLocationHash(); + fixLocationHash() - const language = getDefaultLanguage(available); + const language = getDefaultLanguage(available) this.state = { initializing: true, // For Storage language, theme: false, // Use defined by user in browser - tutorialSeen: false, - }; + tutorialSeen: false + } } // Called after Storage hydrated the component - stopInitializing = () => this.setState({ initializing: false }); + stopInitializing = () => this.setState({ initializing: false }) // Handle global configuration options - handleLanguageChange = (language) => this.setState({ language }); - handleThemeChange = (theme) => this.setState({ theme }); - handleTutorialSeenChange = (tutorialSeen) => this.setState({ tutorialSeen }); + handleLanguageChange = (language) => this.setState({ language }) + handleThemeChange = (theme) => this.setState({ theme }) + handleTutorialSeenChange = (tutorialSeen) => this.setState({ tutorialSeen }) - render() { - const { language, theme, tutorialSeen } = this.state; + render () { + const { language, theme, tutorialSeen } = this.state const translations = available.find( (_language) => _language.key === language - ).value; + ).value return ( {/* Persistent state saver into localStorage */} - + {/* Shows the app, with ErrorBoundaries */} - + - + - + @@ -125,9 +125,9 @@ class App extends React.Component { - ); + ) } } -export default App; -export { fixLocationHash, getDefaultLanguage }; // For tests +export default App +export { fixLocationHash, getDefaultLanguage } // For tests diff --git a/app/src/App.test.js b/app/src/App.test.js index d450dbc5..23f57454 100644 --- a/app/src/App.test.js +++ b/app/src/App.test.js @@ -1,154 +1,154 @@ -import React from "react"; -import { render, fireEvent, act } from "@testing-library/react"; +import React from 'react' +import { render, fireEvent, act } from '@testing-library/react' -import "./testSetup"; -import { delay } from "./testHelpers"; -import App, { fixLocationHash, getDefaultLanguage } from "./App"; +import './testSetup' +import { delay } from './testHelpers' +import App, { fixLocationHash, getDefaultLanguage } from './App' // Mock Dashboard and Widget/List, as this is not needed to test it here -jest.mock("./Dashboard", () => { +jest.mock('./Dashboard', () => { return { __esModule: true, default: (props) => ( -
+
-
+
{JSON.stringify(props.language)}
-
+
{JSON.stringify(props.theme)}
-
+
{JSON.stringify(props.tutorialSeen)}
- ), - }; -}); + ) + } +}) -jest.mock("./Widget", () => { +jest.mock('./Widget', () => { return { __esModule: true, - WidgetsList: () =>
WidgetsList
, - }; -}); + WidgetsList: () =>
WidgetsList
+ } +}) -jest.mock("./Backend", () => { +jest.mock('./Backend', () => { return { __esModule: true, BackendProvider: ({ children }) => <>{children}, - IndexesHandler: ({ children }) => <>{children}, - }; -}); + IndexesHandler: ({ children }) => <>{children} + } +}) // Mock available languages -jest.mock("./i18n/available", () => { +jest.mock('./i18n/available', () => { return { __esModule: true, default: [ - { key: "tested", label: "Tested", value: { locale: "en-en" } }, - { key: "ca-es", label: "Tested ca-es", value: { locale: "fr-FR" } }, - { key: "es-es", label: "Tested es-es", value: { locale: "es-es" } }, - ], - }; -}); - -test("fixes location hash", () => { - window.location.hash = "#/%23/"; - fixLocationHash(); - expect(window.location.hash).toBe("#/#/"); -}); - -test("uses default language `ca-es` when navigator language is unknown", () => { - expect(getDefaultLanguage([])).toBe("ca-es"); -}); - -test("detects navigator language", () => { - const languageGetter = jest.spyOn(window.navigator, "language", "get"); - languageGetter.mockReturnValue("es-ES"); - expect(getDefaultLanguage([{ key: "es-es" }])).toBe("es-es"); -}); - -test("renders mocked dashboard", () => { - const app = render(); - const element = app.getByTestId("dashboard-mock"); - expect(element).toBeInTheDocument(); -}); - -test("detects user changed language", async () => { - let app; + { key: 'tested', label: 'Tested', value: { locale: 'en-en' } }, + { key: 'ca-es', label: 'Tested ca-es', value: { locale: 'fr-FR' } }, + { key: 'es-es', label: 'Tested es-es', value: { locale: 'es-es' } } + ] + } +}) + +test('fixes location hash', () => { + window.location.hash = '#/%23/' + fixLocationHash() + expect(window.location.hash).toBe('#/#/') +}) + +test('uses default language `ca-es` when navigator language is unknown', () => { + expect(getDefaultLanguage([])).toBe('ca-es') +}) + +test('detects navigator language', () => { + const languageGetter = jest.spyOn(window.navigator, 'language', 'get') + languageGetter.mockReturnValue('es-ES') + expect(getDefaultLanguage([{ key: 'es-es' }])).toBe('es-es') +}) + +test('renders mocked dashboard', () => { + const app = render() + const element = app.getByTestId('dashboard-mock') + expect(element).toBeInTheDocument() +}) + +test('detects user changed language', async () => { + let app act(() => { - app = render(); - }); + app = render() + }) await act(async () => { // User selects a language - const button = app.getByTestId("dashboard-mock-fn-language-change"); - expect(button).toBeInTheDocument(); - fireEvent.click(button); + const button = app.getByTestId('dashboard-mock-fn-language-change') + expect(button).toBeInTheDocument() + fireEvent.click(button) // Execute events block in current event loop - await delay(0); + await delay(0) - const status = app.getByTestId("dashboard-mock-language"); - expect(status).toHaveTextContent("tested"); - }); -}); + const status = app.getByTestId('dashboard-mock-language') + expect(status).toHaveTextContent('tested') + }) +}) -test("detects user changed theme", async () => { - let app; +test('detects user changed theme', async () => { + let app act(() => { - app = render(); - }); + app = render() + }) await act(async () => { // User selects a theme - const button = app.getByTestId("dashboard-mock-fn-theme-change"); - expect(button).toBeInTheDocument(); - fireEvent.click(button); + const button = app.getByTestId('dashboard-mock-fn-theme-change') + expect(button).toBeInTheDocument() + fireEvent.click(button) // Execute events block in current event loop - await delay(0); + await delay(0) - const status = app.getByTestId("dashboard-mock-theme"); - expect(status).toHaveTextContent("tested"); - }); -}); + const status = app.getByTestId('dashboard-mock-theme') + expect(status).toHaveTextContent('tested') + }) +}) -test("detects user visited tutorial", async () => { - let app; +test('detects user visited tutorial', async () => { + let app act(() => { - app = render(); - }); + app = render() + }) await act(async () => { // User selects a theme - const button = app.getByTestId("dashboard-mock-fn-tutorial-seen"); - expect(button).toBeInTheDocument(); - fireEvent.click(button); + const button = app.getByTestId('dashboard-mock-fn-tutorial-seen') + expect(button).toBeInTheDocument() + fireEvent.click(button) // Execute events block in current event loop - await delay(0); + await delay(0) - const status = app.getByTestId("dashboard-mock-tutorial-seen"); - expect(status).toHaveTextContent("tested"); - }); -}); + const status = app.getByTestId('dashboard-mock-tutorial-seen') + expect(status).toHaveTextContent('tested') + }) +}) diff --git a/app/src/Backend/Base/Cache.js b/app/src/Backend/Base/Cache.js index 02c57edb..047239eb 100644 --- a/app/src/Backend/Base/Cache.js +++ b/app/src/Backend/Base/Cache.js @@ -1,108 +1,108 @@ -import Common, { log } from "./Common"; +import Common, { log } from './Common' // Transform a string into a smaller/hash one of it const hashStr = (str) => { if (str.length === 0) { - return 0; + return 0 } return [...str] .map((char) => char.charCodeAt(0)) .reduce((hash, int) => { - const hashTmp = (hash << 5) - hash + int; - return hashTmp & hashTmp; // Convert to 32bit integer - }, 0); -}; + const hashTmp = (hash << 5) - hash + int + return hashTmp & hashTmp // Convert to 32bit integer + }, 0) +} // Handles an element inside the cache class FetchCacheElement extends Common { - name = "Cache Element"; - fetching = false; - result = null; - lastModified = 0; - invalidated = false; - listeners = []; - url = ""; - - constructor(url) { - super(); - this.url = url; + name = 'Cache Element' + fetching = false + result = null + lastModified = 0 + invalidated = false + listeners = [] + url = '' + + constructor (url) { + super() + this.url = url } // Handle a fetch error by calling all the listeners' onError in the queue onError = (error) => { - this.listeners.forEach((listener) => listener.onError(error)); - this.cleanFetch(); - }; + this.listeners.forEach((listener) => listener.onError(error)) + this.cleanFetch() + } // Add `this.url` to error messages catchFetchErrorsMessage = (err) => - `${this.url}: ${this.name} backend: ${err.message}`; + `${this.url}: ${this.name} backend: ${err.message}` // Removes all unneeded data related to a fetch - cleanFetch = () => (this.fetching = false); + cleanFetch = () => (this.fetching = false) // Registers a listener addListener = (onSuccess, onError) => { this.listeners.push({ onSuccess, - onError, - }); + onError + }) // If this is the first listener, launch the fetch if (!this.fetching && this.result === null) { - this.fetch(); + this.fetch() } // If we already have the data and it has not been invalidated, create // a self-resolving promise which executes the listener after resolution if (!this.invalidated && this.result !== null) { - new Promise((resolve, reject) => resolve(this.result)).then(onSuccess); + new Promise((resolve, reject) => resolve(this.result)).then(onSuccess) } - this.log(`Added listener to ${this.url}: ${this.listeners.length - 1}`); + this.log(`Added listener to ${this.url}: ${this.listeners.length - 1}`) - return () => this.removeListener(onSuccess); - }; + return () => this.removeListener(onSuccess) + } // Disables/unregisters a listener removeListener = (onSuccess) => { // Finds the listener by comparing the onSuccess function pointer const found = this.listeners .map((listener, index) => ({ listener, index })) - .find((l) => l.listener.onSuccess === onSuccess); + .find((l) => l.listener.onSuccess === onSuccess) if (found) { - this.log(`Remove listener from ${this.url}: ${found.index}`); + this.log(`Remove listener from ${this.url}: ${found.index}`) // Remove element from array - this.listeners.splice(found.index, 1); + this.listeners.splice(found.index, 1) } else { - console.warn(`Remove listener from ${this.url}: Listener not found`); + console.warn(`Remove listener from ${this.url}: Listener not found`) } // If there is an ongoing fetch and there are no more listeners left, abort the fetch if (this.fetching && this.listeners.length === 0) { - this.abort(); - this.cleanFetch(); + this.abort() + this.cleanFetch() } - }; + } // Caches the data and calls all the listeners' onSuccess processSuccessFetch = ({ result, lastModified }) => { - this.result = result; - this.lastModified = lastModified; - this.listeners.forEach((listener) => listener.onSuccess(this.result)); + this.result = result + this.lastModified = lastModified + this.listeners.forEach((listener) => listener.onSuccess(this.result)) - this.cleanFetch(); + this.cleanFetch() // Allow re-chaining promises - return this.result; - }; + return this.result + } // Gets a Date object from a fetch response `last-modified` HTTP header getLastModifiedFromResponse = (response) => - new Date(response.headers.get("last-modified")); + new Date(response.headers.get('last-modified')) // Fetches a URL: // - Transforms from JSON to JS object @@ -111,31 +111,31 @@ class FetchCacheElement extends Common { // - Calls the callback // - If there is an error, processes all error listeners fetch = (callback = () => {}) => { - this.fetching = true; + this.fetching = true return fetch(this.url, { signal: this.controller.signal }) .then(this.handleFetchErrors) .then((response) => response.json().then((result) => ({ result, - lastModified: this.getLastModifiedFromResponse(response), + lastModified: this.getLastModifiedFromResponse(response) })) ) .then(this.processSuccessFetch) .then(callback) .catch(this.catchFetchErrors) - .finally(this.cleanFetch); - }; + .finally(this.cleanFetch) + } // Sends a HEAD request to download URL header's `last-modified` value and // returns the comparison against saved value: `true` if new one is higher checkIfNeedUpdate = (onSuccess, onError) => { - return fetch(this.url, { method: "HEAD", signal: this.controller.signal }) + return fetch(this.url, { method: 'HEAD', signal: this.controller.signal }) .then(this.handleFetchErrors) .then(this.getLastModifiedFromResponse) .then((date) => date > this.lastModified /* || true */) // TEST TODO BUG DEBUG .then(onSuccess) - .catch(onError); - }; + .catch(onError) + } // Invalidates the cache and recalls it's download if we have any listener invalidate = () => { @@ -143,26 +143,26 @@ class FetchCacheElement extends Common { if (this.invalidated) { // Resolve without doing nothing // It has already just been invalidated - this.log(`${this.url}: It has already been invalidated`); - resolve(); + this.log(`${this.url}: It has already been invalidated`) + resolve() } else if (this.result !== null) { - this.invalidated = true; + this.invalidated = true // Are there listeners? if (this.listeners.length > 0) { - this.log(`${this.url}: Fetch it!`); + this.log(`${this.url}: Fetch it!`) this.fetch(() => { - this.invalidated = false; - }); - resolve(); + this.invalidated = false + }) + resolve() } else { // Resolve without doing nothing // Someone downloaded it, but unregistered from it: changed data source this.log( `${this.url}: Someone downloaded it, but unregistered from it: changed data source` - ); - this.invalidated = false; - resolve(); + ) + this.invalidated = false + resolve() } } else { // Resolve without doing nothing @@ -170,13 +170,13 @@ class FetchCacheElement extends Common { this.log(`${this.url}: It was never downloaded`, { result: this.result, invalidated: this.invalidated, - t: this, - }); - this.invalidated = false; - resolve(); + t: this + }) + this.invalidated = false + resolve() } - }); - }; + }) + } } // Handles the whole cache @@ -190,36 +190,36 @@ class FetchCache { ... } */ - data = {}; + data = {} - name = "Cache manager"; - log = log; + name = 'Cache manager' + log = log // Handles fetch requests: // - Creates a new fetch if it's the first time for this URL // - Adds fetch listener if there is an ongoing fetch for that URL // - Returns the result if a fetch for that URL has already been cached fetch = (url, onSuccess, onError = () => {}) => { - const id = hashStr(url); + const id = hashStr(url) // If we have not an ongoing request for this URL, create one if (!(id in this.data)) { - this.data[id] = new FetchCacheElement(url); + this.data[id] = new FetchCacheElement(url) } - return this.data[id].addListener(onSuccess, onError); - }; + return this.data[id].addListener(onSuccess, onError) + } // Sends a HEAD request to download URL header's `last-modified` value and // returns the comparison against saved value: `true` if new one is higher checkIfNeedUpdate = (url, onSuccess, onError) => { - const id = hashStr(url); - return this.data[id].checkIfNeedUpdate(onSuccess, onError); - }; + const id = hashStr(url) + return this.data[id].checkIfNeedUpdate(onSuccess, onError) + } // Invalidates a cache entry and recalls it's download if it has any listener invalidate = (url) => { - const id = hashStr(url); + const id = hashStr(url) if (!(id in this.data)) { return new Promise((resolve, reject) => { // Resolve without doing nothing @@ -227,18 +227,18 @@ class FetchCache { this.log(`${url}: It was never downloaded`, { id, url, - data: this.data, - }); - resolve(true); - }); + data: this.data + }) + resolve(true) + }) } - return this.data[id].invalidate(); - }; + return this.data[id].invalidate() + } } // This is a singleton: // All clients re-use the same cache -const cache = new FetchCache(); -Object.freeze(cache); +const cache = new FetchCache() +Object.freeze(cache) -export default cache; +export default cache diff --git a/app/src/Backend/Base/Common.js b/app/src/Backend/Base/Common.js index 9fcfe41e..680c290e 100644 --- a/app/src/Backend/Base/Common.js +++ b/app/src/Backend/Base/Common.js @@ -1,60 +1,60 @@ -const noop = () => {}; +const noop = () => {} const log = (...args) => { - if (["development", "test"].includes(process.env.NODE_ENV)) { - console.log(...args); + if (['development', 'test'].includes(process.env.NODE_ENV)) { + console.log(...args) } -}; +} class Common { // Visible backend name // Change when subclassing - name = "Common"; + name = 'Common' // Abort controller // Used to stop pending fetches when user changes the date - controller = new AbortController(); + controller = new AbortController() // Overload to do something useful with errors - onError = noop; + onError = noop // Can overload onError also on instantiation time - constructor({ onError } = {}) { - this.onError = onError || this.onError; + constructor ({ onError } = {}) { + this.onError = onError || this.onError } // Abort the abort controller and clean it up creating a new one for next fetches // Add here all side-effects cancelling (fetches, timers, etc) // Remember to call super() when subclassing abort = () => { - this.controller.abort(); - this.controller = new AbortController(); - }; + this.controller.abort() + this.controller = new AbortController() + } // Raises exception on response error handleFetchErrors = (response) => { // Raise succeeded non-ok responses if (!response.ok) { - throw new Error(response.statusText); + throw new Error(response.statusText) } - return response; - }; + return response + } // Catches fetch errors, original or 'self-raised', and throws to `onError` prop // Filters out non-error "Connection aborted" - catchFetchErrorsMessage = (err) => `${this.name} backend: ${err.message}`; - catchFetchErrorsAbortMessage = (err) => "Connection aborted"; + catchFetchErrorsMessage = (err) => `${this.name} backend: ${err.message}` + catchFetchErrorsAbortMessage = (err) => 'Connection aborted' catchFetchErrors = (err) => { - if (err.name === "AbortError") { - console.log(this.catchFetchErrorsAbortMessage()); + if (err.name === 'AbortError') { + console.log(this.catchFetchErrorsAbortMessage()) } else { - err.message = this.catchFetchErrorsMessage(err); - this.onError(err); + err.message = this.catchFetchErrorsMessage(err) + this.onError(err) } - }; + } - log = log; + log = log } -export default Common; -export { log }; +export default Common +export { log } diff --git a/app/src/Backend/Base/Common.test.js b/app/src/Backend/Base/Common.test.js index c3c589e5..2fc016d4 100644 --- a/app/src/Backend/Base/Common.test.js +++ b/app/src/Backend/Base/Common.test.js @@ -2,147 +2,148 @@ import { delay, MockFetch, AbortError, - catchConsoleLog, -} from "../../testHelpers"; + catchConsoleLog +} from '../../testHelpers' -import Common from "./Common"; +import Common from './Common' // Common is suposed to be extended class TestCommon extends Common { - constructor({ onUpdate, ...rest } = {}) { - super(rest); - this.onUpdate = onUpdate || (() => {}); + constructor ({ onUpdate, ...rest } = {}) { + super(rest) + this.onUpdate = onUpdate || (() => {}) } + subscribe = jest.fn((url) => { return fetch(url, { signal: this.controller.signal }) .then(this.handleFetchErrors) .then((response) => response.json()) .then(this.onUpdate) - .catch(this.catchFetchErrors); - }); + .catch(this.catchFetchErrors) + }) } -let mockedFetch; +let mockedFetch beforeAll(() => { - mockedFetch = new MockFetch(); - mockedFetch.mock(); -}); + mockedFetch = new MockFetch() + mockedFetch.mock() +}) beforeEach(() => { - mockedFetch.unmock(); - mockedFetch.mock(); -}); + mockedFetch.unmock() + mockedFetch.mock() +}) afterAll(() => { - mockedFetch.unmock(); -}); + mockedFetch.unmock() +}) -test("Common calls onUpdate", async () => { - const url = "test1"; +test('Common calls onUpdate', async () => { + const url = 'test1' const options = { onUpdate: jest.fn(), - onError: jest.fn(), - }; - const testCommon = new TestCommon(options); - await testCommon.subscribe(url); + onError: jest.fn() + } + const testCommon = new TestCommon(options) + await testCommon.subscribe(url) - expect(options.onUpdate).toHaveBeenCalledTimes(1); -}); + expect(options.onUpdate).toHaveBeenCalledTimes(1) +}) -test("Common calls onError when error is thrown", async () => { - const url = "test1"; +test('Common calls onError when error is thrown', async () => { + const url = 'test1' const options = { onUpdate: jest.fn(), - onError: jest.fn(), - }; - const testCommon = new TestCommon(options); - const fetchThrowErrorOld = mockedFetch.options.throwError; - mockedFetch.options.throwError = new Error("Testing network/server errors"); - await testCommon.subscribe(url); - - expect(options.onError).toHaveBeenCalledTimes(1); - mockedFetch.options.throwError = fetchThrowErrorOld; -}); - -test("Common does not calls onUpdate nor onError when thrown error is an AbortError", async () => { - const url = "test1"; + onError: jest.fn() + } + const testCommon = new TestCommon(options) + const fetchThrowErrorOld = mockedFetch.options.throwError + mockedFetch.options.throwError = new Error('Testing network/server errors') + await testCommon.subscribe(url) + + expect(options.onError).toHaveBeenCalledTimes(1) + mockedFetch.options.throwError = fetchThrowErrorOld +}) + +test('Common does not calls onUpdate nor onError when thrown error is an AbortError', async () => { + const url = 'test1' const options = { onUpdate: jest.fn(), - onError: jest.fn(), - }; - const testCommon = new TestCommon(options); - const fetchThrowErrorOld = mockedFetch.options.throwError; - mockedFetch.options.throwError = new AbortError("Testing abort errors"); + onError: jest.fn() + } + const testCommon = new TestCommon(options) + const fetchThrowErrorOld = mockedFetch.options.throwError + mockedFetch.options.throwError = new AbortError('Testing abort errors') const { output } = await catchConsoleLog(async () => { - await testCommon.subscribe(url); - }); + await testCommon.subscribe(url) + }) - expect(options.onUpdate).toHaveBeenCalledTimes(0); - expect(options.onError).toHaveBeenCalledTimes(0); - expect(output[0].includes("Connection aborted")).toBe(true); - mockedFetch.options.throwError = fetchThrowErrorOld; -}); + expect(options.onUpdate).toHaveBeenCalledTimes(0) + expect(options.onError).toHaveBeenCalledTimes(0) + expect(output[0].includes('Connection aborted')).toBe(true) + mockedFetch.options.throwError = fetchThrowErrorOld +}) -test("Common calls onError when response is errorish", async () => { - const url = "test1"; +test('Common calls onError when response is errorish', async () => { + const url = 'test1' const options = { onUpdate: jest.fn(), - onError: jest.fn(), - }; - const testCommon = new TestCommon(options); + onError: jest.fn() + } + const testCommon = new TestCommon(options) // Test server error - const fetchResponseOptionsOld = mockedFetch.options.responseOptions; + const fetchResponseOptionsOld = mockedFetch.options.responseOptions mockedFetch.options.responseOptions = () => ({ status: 401, - statusText: "Testing unauthorized request", - ok: false, - }); - await testCommon.subscribe(url); + statusText: 'Testing unauthorized request', + ok: false + }) + await testCommon.subscribe(url) - expect(options.onError).toHaveBeenCalledTimes(1); - mockedFetch.options.responseOptions = fetchResponseOptionsOld; -}); + expect(options.onError).toHaveBeenCalledTimes(1) + mockedFetch.options.responseOptions = fetchResponseOptionsOld +}) -test("Common uses noop default values for onUpdate and onError", async () => { - const url = "test1"; - const testCommon = new TestCommon(); +test('Common uses noop default values for onUpdate and onError', async () => { + const url = 'test1' + const testCommon = new TestCommon() // Test for onUpdate - const onUpdateOriginal = testCommon.onUpdate; - testCommon.onUpdate = jest.fn(onUpdateOriginal); - await testCommon.subscribe(url); + const onUpdateOriginal = testCommon.onUpdate + testCommon.onUpdate = jest.fn(onUpdateOriginal) + await testCommon.subscribe(url) - expect(testCommon.onUpdate).toHaveBeenCalledTimes(1); + expect(testCommon.onUpdate).toHaveBeenCalledTimes(1) // Test for onError - const onErrorOriginal = testCommon.onError; - testCommon.onError = jest.fn(onErrorOriginal); - const fetchThrowErrorOld = mockedFetch.options.throwError; - mockedFetch.options.throwError = new Error("Testing network/server errors"); - await testCommon.subscribe(url); + const onErrorOriginal = testCommon.onError + testCommon.onError = jest.fn(onErrorOriginal) + const fetchThrowErrorOld = mockedFetch.options.throwError + mockedFetch.options.throwError = new Error('Testing network/server errors') + await testCommon.subscribe(url) - expect(testCommon.onError).toHaveBeenCalledTimes(1); - mockedFetch.options.throwError = fetchThrowErrorOld; -}); + expect(testCommon.onError).toHaveBeenCalledTimes(1) + mockedFetch.options.throwError = fetchThrowErrorOld +}) -test("Common aborts a connection correctly", async () => { - const url = "test1"; - const testCommon = new TestCommon(); +test('Common aborts a connection correctly', async () => { + const url = 'test1' + const testCommon = new TestCommon() // Mock controller's abort method to count its calls - const abortOriginal = testCommon.controller.abort; - const abortMocked = jest.fn(abortOriginal); - testCommon.controller.abort = abortMocked; - const promise = testCommon.subscribe(url); - await delay(1); + const abortOriginal = testCommon.controller.abort + const abortMocked = jest.fn(abortOriginal) + testCommon.controller.abort = abortMocked + const promise = testCommon.subscribe(url) + await delay(1) - testCommon.abort(); + testCommon.abort() // Ensure promise is not left in background - await promise; + await promise // Controller's abort has been called once - expect(abortMocked).toHaveBeenCalledTimes(1); + expect(abortMocked).toHaveBeenCalledTimes(1) // Controller has been substituted, so abort function // must not be the same as before - expect(abortMocked).not.toBe(testCommon.controller.abort); -}); + expect(abortMocked).not.toBe(testCommon.controller.abort) +}) diff --git a/app/src/Backend/Base/ContextCreator.jsx b/app/src/Backend/Base/ContextCreator.jsx index 7ae9bc6a..151de77e 100644 --- a/app/src/Backend/Base/ContextCreator.jsx +++ b/app/src/Backend/Base/ContextCreator.jsx @@ -21,14 +21,11 @@ const ContextCreator = (Handler, defaultName) => { const withHandler = (WrappedComponent, name = defaultName) => - (props) => - ( - - {(context) => ( - - )} - - ) + (props) => ( + + {(context) => } + + ) return { Provider, diff --git a/app/src/Backend/Base/GHPages.js b/app/src/Backend/Base/GHPages.js index 5acc442b..c4772f83 100644 --- a/app/src/Backend/Base/GHPages.js +++ b/app/src/Backend/Base/GHPages.js @@ -1,20 +1,20 @@ -import Common from "./Common"; -import cache from "./Cache"; +import Common from './Common' +import cache from './Cache' // Handle data backend and cache for backends at GH Pages // This is not a singleton: unique error handlers class GHPages extends Common { // Visible backend name - name = "GitHub Pages"; + name = 'GitHub Pages' // Used to update the data at official schedule // Official schedule's at 10am. It often is some minutes later. // GH Pages cache backend Workflow schedule is at 10:30 and lasts few minutes (less than 5). // We schedule at 10:35 - timerDataUpdate = false; - officialUpdateTime = "10:35".split(":"); + timerDataUpdate = false + officialUpdateTime = '10:35'.split(':') - indexUrl = ""; // Needs to be declared in extended classes + indexUrl = '' // Needs to be declared in extended classes /* Handle data update and cache invalidation @@ -26,69 +26,69 @@ class GHPages extends Common { return cache.checkIfNeedUpdate( this.indexUrl, async (updateNeeded) => { - this.log(`${this.name}: update needed: ${updateNeeded}`); - await callback(updateNeeded); + this.log(`${this.name}: update needed: ${updateNeeded}`) + await callback(updateNeeded) }, (error) => { - console.error(`${this.name}: updating data from backend:`, error); - onError(error); + console.error(`${this.name}: updating data from backend:`, error) + onError(error) } - ); - }; + ) + } // Invalidate all URLs, except index invalidateAll = async () => { console.warn( `${this.name}: Need to define abstract function 'invalidateAll'` - ); - //await cache.invalidate( this.indexUrl ); - }; + ) + // await cache.invalidate( this.indexUrl ); + } // Updates all sources, sequentially updateAll = (callback) => { return (async (callback) => { // Invalidate each active JSON first - await this.invalidateAll(); + await this.invalidateAll() // Finally, invalidate the `index` JSON - this.log(`${this.name}: Invalidate index`); + this.log(`${this.name}: Invalidate index`) - await cache.invalidate(this.indexUrl); + await cache.invalidate(this.indexUrl) - callback(true); - })(callback); - }; + callback(true) + })(callback) + } // Updates all sources if needed // Always calls callback with a boolean argument indicating if an update was made updateIfNeeded = (callback) => { return this.checkUpdate(async (updateNeeded) => { if (updateNeeded) { - await this.updateAll(() => callback(true)); + await this.updateAll(() => callback(true)) } else { - callback(false); + callback(false) } - }); - }; + }) + } // Cancels an ongoing timer for update cancelUpdateSchedule = () => { if (this.timerDataUpdate) { - clearTimeout(this.timerDataUpdate); - this.timerDataUpdate = false; + clearTimeout(this.timerDataUpdate) + this.timerDataUpdate = false } - }; + } // Try to update data on next official scheduled data update scheduleNextUpdate = ({ millis = false, ...options } = {}) => { // If millis is not defined, call on next calculated default - const nextMillis = millis === false ? this.millisToNextUpdate() : millis; + const nextMillis = millis === false ? this.millisToNextUpdate() : millis - const nextUpdateDate = new Date(new Date().getTime() + nextMillis); - this.log(`${this.name}: Next update on ${nextUpdateDate}`); + const nextUpdateDate = new Date(new Date().getTime() + nextMillis) + this.log(`${this.name}: Next update on ${nextUpdateDate}`) // Just in case - this.cancelUpdateSchedule(); + this.cancelUpdateSchedule() // If data has been updated and `recursive` is true, re-schedule data update for the next day // Else (recursive || not recursive but data NOT updated), schedule data update in 5 minutes @@ -98,35 +98,35 @@ class GHPages extends Common { recursive = false, onBeforeUpdate = () => {}, onAfterUpdate = () => {}, - notUpdatedMillis = 300_000, - } = options; + notUpdatedMillis = 300_000 + } = options - onBeforeUpdate(); + onBeforeUpdate() this.updateIfNeeded((updated) => { - onAfterUpdate(updated); + onAfterUpdate(updated) if (recursive || !updated) { this.scheduleNextUpdate({ ...options, - ...(updated ? {} : { millis: notUpdatedMillis }), - }); + ...(updated ? {} : { millis: notUpdatedMillis }) + }) } - }); - }, nextMillis); + }) + }, nextMillis) - return nextUpdateDate; - }; + return nextUpdateDate + } abort = () => { - this.cancelUpdateSchedule(); - super.abort(); - }; + this.cancelUpdateSchedule() + super.abort() + } // Calculates haw many milliseconds until next schedulled update (today's or tomorrow) // TODO: Take care of timezones: Official date is in CEST/GMT+0200 (with daylight saving modifications), Date uses user's timezone and returns UTC // Now, it only works if user timezone is CEST // Probably, the best would be to translate both dates into UTC and, only then, compare them millisToNextUpdate = () => { - const now = new Date(); + const now = new Date() const todayDataSchedule = new Date( now.getFullYear(), now.getMonth(), @@ -134,13 +134,13 @@ class GHPages extends Common { ...this.officialUpdateTime, 0, 0 - ); - const millisTillSchedulle = todayDataSchedule - now; + ) + const millisTillSchedulle = todayDataSchedule - now return millisTillSchedulle <= 0 ? millisTillSchedulle + 86_400_000 // it's on or after today's schedule, try next schedule tomorrow. - : millisTillSchedulle; - }; + : millisTillSchedulle + } } -export default GHPages; +export default GHPages diff --git a/app/src/Backend/Base/GHPages.test.js b/app/src/Backend/Base/GHPages.test.js index 0f6d557b..f1f907fd 100644 --- a/app/src/Backend/Base/GHPages.test.js +++ b/app/src/Backend/Base/GHPages.test.js @@ -2,10 +2,10 @@ import { delay, catchConsoleLog, catchConsoleWarn, - catchConsoleError, -} from "../../testHelpers"; + catchConsoleError +} from '../../testHelpers' -import GHPages from "./GHPages"; +import GHPages from './GHPages' /* TODO: @@ -14,186 +14,186 @@ import GHPages from "./GHPages"; - Test `millisToNextUpdate` with an extra day timelapse (`officialUpdateTime` on today, but earlier than now) - Test `abort` */ -const mockDelay = delay; +const mockDelay = delay class MockCache { - success = true; - successValue = true; - errorValue = new Error("Test cache checkIfNeedUpdate error"); + success = true + successValue = true + errorValue = new Error('Test cache checkIfNeedUpdate error') run = async (url, onSuccess, onError) => { - await mockDelay(10); + await mockDelay(10) if (this.success) { - await onSuccess(this.successValue); + await onSuccess(this.successValue) } else { - await onError(this.errorValue); + await onError(this.errorValue) } - }; + } } -const mockCache = new MockCache(); -jest.mock("./Cache", () => { +const mockCache = new MockCache() +jest.mock('./Cache', () => { return { __esModule: true, default: { invalidate: jest.fn(async (url) => { - await mockDelay(10); + await mockDelay(10) }), - checkIfNeedUpdate: (...args) => mockCache.run(...args), - }, - }; -}); + checkIfNeedUpdate: (...args) => mockCache.run(...args) + } + } +}) class TestGHPages extends GHPages { - indexUrl = "testIndex"; + indexUrl = 'testIndex' // Invalidate all URLs, except index invalidateAll = jest.fn(async () => { - await delay(10); - }); + await delay(10) + }) } -test("GHPages correctly checks for updates", async () => { - const testGHPages = new TestGHPages(); - const onSuccess = jest.fn(() => console.log("SUCCESS")); - const onError = jest.fn((err) => console.error("ERROR:", err)); +test('GHPages correctly checks for updates', async () => { + const testGHPages = new TestGHPages() + const onSuccess = jest.fn(() => console.log('SUCCESS')) + const onError = jest.fn((err) => console.error('ERROR:', err)) // Test success with update needed const { output: outputSuccess } = await catchConsoleLog( async () => await testGHPages.checkUpdate(onSuccess, onError) - ); - expect(onSuccess).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledTimes(0); - expect(onSuccess).toHaveBeenCalledWith(mockCache.successValue); - expect(outputSuccess[0].includes("update needed")).toBe(true); + ) + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledTimes(0) + expect(onSuccess).toHaveBeenCalledWith(mockCache.successValue) + expect(outputSuccess[0].includes('update needed')).toBe(true) // Test error - mockCache.success = false; + mockCache.success = false const { output: outputError } = await catchConsoleError( async () => await testGHPages.checkUpdate(onSuccess, onError) - ); - expect(onSuccess).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenCalledTimes(1); - expect(outputError[1].includes("Test cache checkIfNeedUpdate error")).toBe( + ) + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onError).toHaveBeenCalledTimes(1) + expect(outputError[1].includes('Test cache checkIfNeedUpdate error')).toBe( true - ); - mockCache.success = true; -}); + ) + mockCache.success = true +}) -test("GHPages correctly warns about the need to overload `invalidateAll`", async () => { - const testGHPages = new GHPages(); +test('GHPages correctly warns about the need to overload `invalidateAll`', async () => { + const testGHPages = new GHPages() const { output } = await catchConsoleWarn( async () => await testGHPages.invalidateAll() - ); - expect(output[0].includes("abstract function")).toBe(true); -}); + ) + expect(output[0].includes('abstract function')).toBe(true) +}) -test("GHPages correctly updates all own URLs", async () => { - const testGHPages = new TestGHPages(); - const callback = jest.fn(); +test('GHPages correctly updates all own URLs', async () => { + const testGHPages = new TestGHPages() + const callback = jest.fn() const { output } = await catchConsoleLog( async () => await testGHPages.updateAll(callback) - ); - expect(output[0].includes("Invalidate index")).toBe(true); - expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledTimes(1); -}); + ) + expect(output[0].includes('Invalidate index')).toBe(true) + expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(1) +}) -test("GHPages correctly updates all own URLs, if needed", async () => { - const testGHPages = new TestGHPages(); - const callback = jest.fn(); +test('GHPages correctly updates all own URLs, if needed', async () => { + const testGHPages = new TestGHPages() + const callback = jest.fn() // Test no need to update - mockCache.successValue = false; + mockCache.successValue = false const { output } = await catchConsoleLog( async () => await testGHPages.updateIfNeeded(callback) - ); - expect(output[0].includes("update needed: false")).toBe(true); - expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(0); - expect(callback).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledWith(false); - mockCache.successValue = true; + ) + expect(output[0].includes('update needed: false')).toBe(true) + expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(0) + expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledWith(false) + mockCache.successValue = true // Test need to update const { output: output2 } = await catchConsoleLog(async () => { - await testGHPages.updateIfNeeded(callback); - }); - expect(output2[0].includes("update needed: true")).toBe(true); - expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(1); - expect(callback).toHaveBeenCalledTimes(2); - expect(callback).toHaveBeenCalledWith(true); -}); + await testGHPages.updateIfNeeded(callback) + }) + expect(output2[0].includes('update needed: true')).toBe(true) + expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledTimes(2) + expect(callback).toHaveBeenCalledWith(true) +}) -test("GHPages correctly schedules next update loop", async () => { - const testGHPages = new TestGHPages(); +test('GHPages correctly schedules next update loop', async () => { + const testGHPages = new TestGHPages() // Generate a date in the near future - const future = new Date(); - future.setMinutes(future.getMinutes() + 2); + const future = new Date() + future.setMinutes(future.getMinutes() + 2) testGHPages.officialUpdateTime = [ future.getHours(), future.getMinutes(), - future.getSeconds(), - ]; + future.getSeconds() + ] // Generate a string with the generated date, prepending '0' to each value if it's smaller than 10 - const pad2 = (num) => `${num < 10 ? "0" : ""}${num}`; - const expectedTimeString = testGHPages.officialUpdateTime.map(pad2).join(":"); + const pad2 = (num) => `${num < 10 ? '0' : ''}${num}` + const expectedTimeString = testGHPages.officialUpdateTime.map(pad2).join(':') // First try: update on mocked schedule const { output } = await catchConsoleLog(async () => { - testGHPages.scheduleNextUpdate(); - }); - expect(output[0].includes("Next update on")).toBe(true); - expect(output[0].includes(expectedTimeString)).toBe(true); - expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(0); + testGHPages.scheduleNextUpdate() + }) + expect(output[0].includes('Next update on')).toBe(true) + expect(output[0].includes(expectedTimeString)).toBe(true) + expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(0) - testGHPages.cancelUpdateSchedule(); + testGHPages.cancelUpdateSchedule() // Second try: update on `nextMillis` milliseconds - const nextMillis = 20; + const nextMillis = 20 const options = { millis: nextMillis, notUpdatedMillis: 10, onBeforeUpdate: jest.fn(), - onAfterUpdate: jest.fn(), - }; + onAfterUpdate: jest.fn() + } const { output: output2 } = await catchConsoleLog(async () => { - testGHPages.scheduleNextUpdate(options); - }); - expect(output2[0].includes("Next update on")).toBe(true); + testGHPages.scheduleNextUpdate(options) + }) + expect(output2[0].includes('Next update on')).toBe(true) // Should not have been updated yet (before wait) - expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(0); - expect(options.onBeforeUpdate).toHaveBeenCalledTimes(0); - expect(options.onAfterUpdate).toHaveBeenCalledTimes(0); + expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(0) + expect(options.onBeforeUpdate).toHaveBeenCalledTimes(0) + expect(options.onAfterUpdate).toHaveBeenCalledTimes(0) // Silence output - const checkUpdateOld = testGHPages.checkUpdate.bind(testGHPages); + const checkUpdateOld = testGHPages.checkUpdate.bind(testGHPages) testGHPages.checkUpdate = jest.fn(async (...args) => { await catchConsoleLog(async () => { - await checkUpdateOld(...args); - }); - }); + await checkUpdateOld(...args) + }) + }) // Force to recurse to next try (no update needed yet) - mockCache.successValue = false; - await delay(nextMillis); - expect(options.onBeforeUpdate).toHaveBeenCalledTimes(1); - expect(options.onAfterUpdate).toHaveBeenCalledTimes(0); + mockCache.successValue = false + await delay(nextMillis) + expect(options.onBeforeUpdate).toHaveBeenCalledTimes(1) + expect(options.onAfterUpdate).toHaveBeenCalledTimes(0) - await delay(10); - expect(options.onBeforeUpdate).toHaveBeenCalledTimes(1); - expect(options.onAfterUpdate).toHaveBeenCalledTimes(1); + await delay(10) + expect(options.onBeforeUpdate).toHaveBeenCalledTimes(1) + expect(options.onAfterUpdate).toHaveBeenCalledTimes(1) // Set to need an update - mockCache.successValue = true; - await delay(10); - expect(options.onBeforeUpdate).toHaveBeenCalledTimes(2); - expect(options.onAfterUpdate).toHaveBeenCalledTimes(1); + mockCache.successValue = true + await delay(10) + expect(options.onBeforeUpdate).toHaveBeenCalledTimes(2) + expect(options.onAfterUpdate).toHaveBeenCalledTimes(1) - await delay(nextMillis + 15); - expect(options.onBeforeUpdate).toHaveBeenCalledTimes(2); - expect(options.onAfterUpdate).toHaveBeenCalledTimes(2); + await delay(nextMillis + 15) + expect(options.onBeforeUpdate).toHaveBeenCalledTimes(2) + expect(options.onAfterUpdate).toHaveBeenCalledTimes(2) // Should already have been updated (after wait) - expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(1); -}); + expect(testGHPages.invalidateAll).toHaveBeenCalledTimes(1) +}) diff --git a/app/src/Backend/Base/withHandlerGenerator.jsx b/app/src/Backend/Base/withHandlerGenerator.jsx index 69de8013..76d1ee3a 100644 --- a/app/src/Backend/Base/withHandlerGenerator.jsx +++ b/app/src/Backend/Base/withHandlerGenerator.jsx @@ -34,11 +34,13 @@ const withHandlerGenerator = ( [BackendHandler, params] ) - return value === false ? ( - - ) : ( - - ) + return value === false + ? ( + + ) + : ( + + ) } // Return wrapper component wrapped with a diff --git a/app/src/Backend/Base/withHandlerGenerator.test.jsx b/app/src/Backend/Base/withHandlerGenerator.test.jsx index 62c01aef..f8065a6f 100644 --- a/app/src/Backend/Base/withHandlerGenerator.test.jsx +++ b/app/src/Backend/Base/withHandlerGenerator.test.jsx @@ -1,130 +1,129 @@ -import React from "react"; +import React from 'react' import { render, createEvent, fireEvent, act, screen, - within, -} from "@testing-library/react"; + within +} from '@testing-library/react' -import { delay } from "../../testHelpers"; -import withHandlerGenerator from "./withHandlerGenerator"; +import { delay } from '../../testHelpers' +import withHandlerGenerator from './withHandlerGenerator' // Mock the component -jest.mock("../../Loading", () => { +jest.mock('../../Loading', () => { return { __esModule: true, - default: (props) =>
Loading
, - }; -}); + default: (props) =>
Loading
+ } +}) // Mocked backend handler, like in src/Backend/*/index.js class BackendHandler { - constructor(testParam) { - this.testParam = testParam; + constructor (testParam) { + this.testParam = testParam } index = jest.fn((callback) => { - const { testParam } = this; - const timer = setTimeout(callback({ role: "tested", testParam }), 20); - return () => clearTimeout(timer); - }); + const { testParam } = this + const timer = setTimeout(callback({ role: 'tested', testParam }), 20) + return () => clearTimeout(timer) + }) } // Mocked backend handler HOC -const withBackendHandlerHOC = (Wrapped) => (props) => - ( -
- -
- ); - -test("withHandlerGenerator correctly generates a HOC to create a Wrapped component with data as a prop", async () => { +const withBackendHandlerHOC = (Wrapped) => (props) => ( +
+ +
+) + +test('withHandlerGenerator correctly generates a HOC to create a Wrapped component with data as a prop', async () => { // Generate a testing HOC - const withIndex = (WrappedComponent, name = "index") => + const withIndex = (WrappedComponent, name = 'index') => withHandlerGenerator( withBackendHandlerHOC, ({ testParam }) => ({ testParam }), ({ testParam }, Handler, setIndex) => { - const handler = new Handler(testParam); - return handler.index(setIndex); + const handler = new Handler(testParam) + return handler.index(setIndex) }, name, WrappedComponent - ); + ) // Use the generated testing HOC to pass `index` as a prop const TestComponent = withIndex( ({ index: index_, role, testParam, ...props }) => { - const { testParam: testParamIndex, ...index } = index_; + const { testParam: testParamIndex, ...index } = index_ return ( - - - + + + - ); + ) } - ); + ) - let rendered; + let rendered await act(async () => { - const paramValue = "test-value"; + const paramValue = 'test-value' rendered = render( - - ); + + ) // Initial state - const wrapperInitial = rendered.getByRole("wrapper"); - expect(wrapperInitial).toBeInTheDocument(); + const wrapperInitial = rendered.getByRole('wrapper') + expect(wrapperInitial).toBeInTheDocument() - const loading = within(wrapperInitial).getByRole("loading"); - expect(loading).toBeInTheDocument(); + const loading = within(wrapperInitial).getByRole('loading') + expect(loading).toBeInTheDocument() - await delay(20); + await delay(20) // Final state, with `index` as a prop in the wrapped component - const wrapper = rendered.getByRole("wrapper"); - expect(wrapper).toBeInTheDocument(); + const wrapper = rendered.getByRole('wrapper') + expect(wrapper).toBeInTheDocument() - const wrapped = within(wrapper).getByRole("test-component"); - expect(wrapped).toBeInTheDocument(); + const wrapped = within(wrapper).getByRole('test-component') + expect(wrapped).toBeInTheDocument() - const index = within(wrapped).getByRole("tested"); - expect(index).toBeInTheDocument(); - expect(index.getAttribute("data-testid")).toBe("index"); + const index = within(wrapped).getByRole('tested') + expect(index).toBeInTheDocument() + expect(index.getAttribute('data-testid')).toBe('index') - const testParam = within(wrapped).getByTestId("testParam"); - expect(testParam).toBeInTheDocument(); + const testParam = within(wrapped).getByTestId('testParam') + expect(testParam).toBeInTheDocument() - const testParamIndex = within(wrapped).getByTestId("testParamIndex"); - expect(testParamIndex).toBeInTheDocument(); + const testParamIndex = within(wrapped).getByTestId('testParamIndex') + expect(testParamIndex).toBeInTheDocument() - expect(testParamIndex.getAttribute("value")).toBe(paramValue); - expect(testParamIndex.getAttribute("value")).toBe( - testParam.getAttribute("value") - ); - }); + expect(testParamIndex.getAttribute('value')).toBe(paramValue) + expect(testParamIndex.getAttribute('value')).toBe( + testParam.getAttribute('value') + ) + }) await act(async () => { - const paramValue = "test-value-2"; + const paramValue = 'test-value-2' rendered.rerender( - - ); + + ) - await delay(0); + await delay(0) - const testParam = rendered.getByTestId("testParam"); - expect(testParam).toBeInTheDocument(); + const testParam = rendered.getByTestId('testParam') + expect(testParam).toBeInTheDocument() - const testParamIndex = rendered.getByTestId("testParamIndex"); - expect(testParamIndex).toBeInTheDocument(); + const testParamIndex = rendered.getByTestId('testParamIndex') + expect(testParamIndex).toBeInTheDocument() - expect(testParamIndex.getAttribute("value")).toBe(paramValue); - expect(testParamIndex.getAttribute("value")).toBe( - testParam.getAttribute("value") - ); - }); -}); + expect(testParamIndex.getAttribute('value')).toBe(paramValue) + expect(testParamIndex.getAttribute('value')).toBe( + testParam.getAttribute('value') + ) + }) +}) diff --git a/app/src/Backend/Bcn/handler.js b/app/src/Backend/Bcn/handler.js index 9cd5f2e5..a816668e 100644 --- a/app/src/Backend/Bcn/handler.js +++ b/app/src/Backend/Bcn/handler.js @@ -1,67 +1,67 @@ -import GHPages from "../Base/GHPages"; -import cache from "../Base/Cache"; +import GHPages from '../Base/GHPages' +import cache from '../Base/Cache' const BcnStaticHost = process.env.REACT_APP_BCN_STATIC_HOST ?? - "https://emibcn.github.io/covid-data/Bcn"; -const BcnDataStaticURL = `${BcnStaticHost}/index.json`; + 'https://emibcn.github.io/covid-data/Bcn' +const BcnDataStaticURL = `${BcnStaticHost}/index.json` // Handle data backend and cache for BCN class BcnDataHandler extends GHPages { // Visible backend name - name = "Barcelona JSON Files"; - indexUrl = BcnDataStaticURL; - unsubscribeIndex = () => {}; + name = 'Barcelona JSON Files' + indexUrl = BcnDataStaticURL + unsubscribeIndex = () => {} // If index data is not passed, subscribe to index // URL and parse its content once downloaded - constructor(index) { - super(); + constructor (index) { + super() if (index) { - this.parseIndex(index); + this.parseIndex(index) } else { - this.unsubscribeIndex = this.indexInternal(this.parseIndex); + this.unsubscribeIndex = this.indexInternal(this.parseIndex) } } /* Return dynamic data (with cache) */ - indexData = []; - indexInternal = (callback) => cache.fetch(BcnDataStaticURL, callback); + indexData = [] + indexInternal = (callback) => cache.fetch(BcnDataStaticURL, callback) // Return unsubscription callback // Return unsubscription from possible automatic download index = (callback) => { - const unsubscribe = this.indexInternal(callback); + const unsubscribe = this.indexInternal(callback) return () => { - unsubscribe(); - this.unsubscribeIndex(); - }; - }; + unsubscribe() + this.unsubscribeIndex() + } + } parseIndex = (index) => { // Save/cache index data - this.indexData = index; + this.indexData = index // Update active downloads - this.invalidateAll(); - }; + this.invalidateAll() + } // Active URLs: those which will be invalidated on update - active = []; + active = [] // Invalidate all URLs, except index invalidateAll = async () => { for (const url of this.active) { - if (process.env.NODE_ENV === "development") { - console.log(`${this.name}: Invalidate '?${url}'`); + if (process.env.NODE_ENV === 'development') { + console.log(`${this.name}: Invalidate '?${url}'`) } - await cache.invalidate(`${BcnStaticHost}/${url}`); + await cache.invalidate(`${BcnStaticHost}/${url}`) } - }; + } // Gets the breadcrumb of ancestors and the found node, or empty array (recursive) findBreadcrumb = ( @@ -70,63 +70,63 @@ class BcnDataHandler extends GHPages { compare = ({ code }, value) => code === value ) => { if (node === null) { - node = { sections: this.indexData }; + node = { sections: this.indexData } } if (compare(node, value)) { - return [node]; - } else if ("sections" in node) { + return [node] + } else if ('sections' in node) { for (const child of node.sections) { - const found = this.findBreadcrumb(child, value, compare); + const found = this.findBreadcrumb(child, value, compare) if (found.length) { - return [...found, node]; + return [...found, node] } } } - return []; - }; + return [] + } // Find a node in a tree findChild = (node, value, compare) => { - const list = this.findBreadcrumb(node, value, compare); + const list = this.findBreadcrumb(node, value, compare) if (list.length) { - return list[0]; + return list[0] } - }; + } // Find division/population section findInitialNode = (dataset) => { - return this.indexData.find(({ code }) => code === dataset); - }; + return this.indexData.find(({ code }) => code === dataset) + } // Return filtered index data values - filter = (fn) => this.indexData.filter(fn); + filter = (fn) => this.indexData.filter(fn) // Active URLs: those which will be invalidated on update - active = []; + active = [] // Fetch JSON data and subscribe to updates data = (dataset, callback) => { // Find region name in link and children, recursively - const found = this.findChild(null, dataset); + const found = this.findChild(null, dataset) // TODO: We should do something else on error if (!found) { console.warn(`Could not find data in index: ${dataset}`, { - index: this.indexData, - }); - callback(false); - return () => {}; + index: this.indexData + }) + callback(false) + return () => {} } // Add URL to active ones. Will be invalidated on update if (!this.active.includes(found.values)) { - this.active.push(found.values); + this.active.push(found.values) } // Get URL content (download or cached) // Return unsubscription callback - return cache.fetch(`${BcnStaticHost}/${found.values}`, callback); - }; + return cache.fetch(`${BcnStaticHost}/${found.values}`, callback) + } } -export default BcnDataHandler; +export default BcnDataHandler diff --git a/app/src/Backend/Charts/handler.js b/app/src/Backend/Charts/handler.js index 789e67d3..7fe0abc3 100644 --- a/app/src/Backend/Charts/handler.js +++ b/app/src/Backend/Charts/handler.js @@ -1,129 +1,129 @@ -import GHPages from "../Base/GHPages"; -import cache from "../Base/Cache"; +import GHPages from '../Base/GHPages' +import cache from '../Base/Cache' const ChartStaticHost = process.env.REACT_APP_CHART_STATIC_HOST ?? - "https://emibcn.github.io/covid-data/Charts"; -const ChartDataStaticURL = `${ChartStaticHost}/index.json`; -const ChartDataBase = `${ChartStaticHost}/chart.json`; + 'https://emibcn.github.io/covid-data/Charts' +const ChartDataStaticURL = `${ChartStaticHost}/index.json` +const ChartDataBase = `${ChartStaticHost}/chart.json` // Handle data backend and cache for Charts // This is not a singleton: unique error handlers class ChartDataHandler extends GHPages { // Visible backend name - name = "Charts JSON Files"; - indexUrl = ChartDataStaticURL; - unsubscribeIndex = () => {}; + name = 'Charts JSON Files' + indexUrl = ChartDataStaticURL + unsubscribeIndex = () => {} /* Processed data from index */ - divisions = []; - populations = []; + divisions = [] + populations = [] // If index data is not passed, subscribe to index // URL and parse its content once downloaded - constructor(index) { - super(); + constructor (index) { + super() if (index) { - this.parseIndex(index); + this.parseIndex(index) } else { - this.unsubscribeIndex = this.indexInternal(this.parseIndex); + this.unsubscribeIndex = this.indexInternal(this.parseIndex) } } /* Return dynamic data (with cache) */ - indexData = []; - indexInternal = (callback) => cache.fetch(ChartDataStaticURL, callback); + indexData = [] + indexInternal = (callback) => cache.fetch(ChartDataStaticURL, callback) // Return unsubscription callback // If automatic index download was done (in constructor), // unsubscribe also from it index = (callback) => { - const unsubscribe = this.indexInternal(callback); + const unsubscribe = this.indexInternal(callback) return () => { - unsubscribe(); - this.unsubscribeIndex(); - }; - }; + unsubscribe() + this.unsubscribeIndex() + } + } parseIndex = (index) => { // Get a unique (`[...new Set( )]`) list of options elements - this.divisions = [...new Set(index.map(({ territori }) => territori))]; - this.populations = [...new Set(index.map(({ poblacio }) => poblacio))]; + this.divisions = [...new Set(index.map(({ territori }) => territori))] + this.populations = [...new Set(index.map(({ poblacio }) => poblacio))] // Save/cache index data - this.indexData = index; + this.indexData = index // Update active downloads - this.invalidateAll(); - }; + this.invalidateAll() + } // Active URLs: those which will be invalidated on update - active = []; + active = [] - getURL = (url) => `${ChartDataBase}${encodeURIComponent(`?${url}`)}`; + getURL = (url) => `${ChartDataBase}${encodeURIComponent(`?${url}`)}` // Invalidate all URLs, except index invalidateAll = async () => { for (const url of this.active) { - this.log(`${this.name}: Invalidate '?${url}'`); - await cache.invalidate(this.getURL(url)); + this.log(`${this.name}: Invalidate '?${url}'`) + await cache.invalidate(this.getURL(url)) } - }; + } // Gets the breadcrumb of ancestors and the found node, or empty array (recursive) findBreadcrumb = (node, value, compare = (node, url) => node.url === url) => { if (compare(node, value)) { - return [node]; - } else if ("children" in node) { + return [node] + } else if ('children' in node) { for (const child of node.children) { - const found = this.findBreadcrumb(child, value, compare); + const found = this.findBreadcrumb(child, value, compare) if (found.length) { - return [...found, node]; + return [...found, node] } } } - return []; - }; + return [] + } // Find a node in a tree findChild = (node, value, compare) => { - const list = this.findBreadcrumb(node, value, compare); + const list = this.findBreadcrumb(node, value, compare) if (list.length) { - return list[0]; + return list[0] } - }; + } // Find division/population section findInitialNode = (division, population) => { return this.indexData.find( (link) => link.territori === division && link.poblacio === population - ); - }; + ) + } find = (division, population, url) => { - const initialLink = this.findInitialNode(division, population); - return this.findChild(initialLink, url); - }; + const initialLink = this.findInitialNode(division, population) + return this.findChild(initialLink, url) + } // Used to fix region when changing main options. // TODO: Find a better way. ID by breadcrumb? findRegion = (division, population, region) => { - const initialNode = this.findInitialNode(division, population); - const found = this.findChild(initialNode, region); + const initialNode = this.findInitialNode(division, population) + const found = this.findChild(initialNode, region) if (!found) { // Try to find in the other valid initialNodes (same division) const nodes = this.indexData.filter( (node) => division === node.territori && population !== node.poblacio - ); + ) for (const node of nodes) { // Look for region in the other initialNode - const f1 = this.findChild(node, region); + const f1 = this.findChild(node, region) // If found, find in our initialNode for a region with the same name const f2 = f1 && @@ -131,56 +131,56 @@ class ChartDataHandler extends GHPages { initialNode, f1.name, (node, name) => node.name === name - ); + ) // If found, use it if (f2) { - return f2; + return f2 } } // Not found in valid initialNodes: default to actual initialNode's root - return initialNode; + return initialNode } // Found! - return found; - }; + return found + } // Fetch JSON data and subscribe to updates data = (division, population, url, callback) => { - const initialLink = this.findInitialNode(division, population); + const initialLink = this.findInitialNode(division, population) // If found (why should it not?) if (initialLink) { // Find region name in link and children, recursively - const found = this.findChild(initialLink, url); + const found = this.findChild(initialLink, url) // TODO: We should do something else on error if (!found) { console.warn( `Could not find data in index: ${division}/${population}/${url}`, { initialLink, index: this.indexData } - ); - callback(false); - return () => {}; + ) + callback(false) + return () => {} } // Add URL to active ones. Will be invalidated on update if (!this.active.includes(found.url)) { - this.active.push(found.url); + this.active.push(found.url) } // Get URL content (download or cached) - return cache.fetch(this.getURL(found.url), callback); + return cache.fetch(this.getURL(found.url), callback) } console.warn( `Could not find initial node in index: ${division}/${population}/${url}`, { initialLink, index: this.indexData } - ); - callback(false); - return () => {}; - }; + ) + callback(false) + return () => {} + } } -export default ChartDataHandler; +export default ChartDataHandler diff --git a/app/src/Backend/IndexesHandler.jsx b/app/src/Backend/IndexesHandler.jsx index 7067525f..a3bd367a 100644 --- a/app/src/Backend/IndexesHandler.jsx +++ b/app/src/Backend/IndexesHandler.jsx @@ -1,14 +1,14 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import Loading from "../Loading"; -import withDocumentVisibility from "../withDocumentVisibility"; +import Loading from '../Loading' +import withDocumentVisibility from '../withDocumentVisibility' -import { withHandler as withMapsDataHandler } from "../Backend/Maps/context"; -import { withHandler as withChartsDataHandler } from "../Backend/Charts/context"; -import { withHandler as withBcnDataHandler } from "../Backend/Bcn/context"; +import { withHandler as withMapsDataHandler } from '../Backend/Maps/context' +import { withHandler as withChartsDataHandler } from '../Backend/Charts/context' +import { withHandler as withBcnDataHandler } from '../Backend/Bcn/context' -import Provider from "./Provider"; +import Provider from './Provider' // Initially downloads all backends indexes // - Shows while data is downloading @@ -29,22 +29,22 @@ import Provider from "./Provider"; // - Unsubscribe to index updates. If listeners go down to 0 and there is an // ongoing download, it is automatically aborted by each backend handler. class IndexesHandler extends React.Component { - constructor(props) { - super(props); + constructor (props) { + super(props) // Create initial handled backends array - this.backends = ["maps", "charts", "bcn"].map((name) => ({ + this.backends = ['maps', 'charts', 'bcn'].map((name) => ({ name, initializer: props[`${name}DataHandler`], state: `${name}DataHandler`, - unsubscribe: () => {}, - })); + unsubscribe: () => {} + })) // Initialize to null an index state for each backend this.state = this.backends.reduce((acc, { state }) => { - acc[state] = null; - return acc; - }, {}); + acc[state] = null + return acc + }, {}) } // TODO: Launch new notification @@ -53,8 +53,8 @@ class IndexesHandler extends React.Component { onBeforeUpdate = (backend) => { console.log( `onBeforeUpdate: Looking for an update of backend ${backend.handler.name}` - ); - }; + ) + } // TODO: Launch new notification // App can stop onBeforeUpdate and resolve to ok/ok depending on `updated` @@ -62,35 +62,35 @@ class IndexesHandler extends React.Component { onAfterUpdate = (backend, updated) => { console.log( `onAfterUpdate: The backend ${backend.handler.name} was${ - updated ? "" : " not" + updated ? '' : ' not' } updated` - ); - }; + ) + } // Checks if we need to update it and, if so, do it updateIfNeeded = (backend) => { return new Promise((resolve) => { // If it has not yet been downloaded, // we definitively still don't need an update - const now = new Date(); + const now = new Date() if ( this.state[backend.state] !== null && !!backend.nextUpdateDate && now > backend.nextUpdateDate ) { - this.onBeforeUpdate(backend); + this.onBeforeUpdate(backend) backend.handler.updateIfNeeded((updated) => { - this.onAfterUpdate(backend, updated); - resolve(backend, updated); - }); + this.onAfterUpdate(backend, updated) + resolve(backend, updated) + }) } else { - resolve(backend, false); + resolve(backend, false) } - }); - }; + }) + } // Return a Promise resolved once all backends have updated if needed - updateAllIfNeeded = () => Promise.all(this.backends.map(this.updateIfNeeded)); + updateAllIfNeeded = () => Promise.all(this.backends.map(this.updateIfNeeded)) // Schedules a backend update and saves its // due date into its `this.backends` object @@ -99,105 +99,107 @@ class IndexesHandler extends React.Component { // A `force now` callback can be passed to allow forcing immediate update // A `cancel` callback can be passed to allow cancelling the scheduled update scheduleNextUpdate = (backend) => { - const { milllisToNextUpdate: notUpdatedMillis } = this.props; + const { milllisToNextUpdate: notUpdatedMillis } = this.props // Mind the timer on unmount // Use the date where needed backend.nextUpdateDate = backend.handler.scheduleNextUpdate({ onBeforeUpdate: this.onBeforeUpdate.bind(this, backend), onAfterUpdate: this.onAfterUpdate.bind(this, backend), - notUpdatedMillis, - }); - }; + notUpdatedMillis + }) + } // Fetch data once mounted - componentDidMount() { + componentDidMount () { // Get `visible` status before the update: if it changes, // we will subscribe to update schedule in componentDidUpdate - const { visible } = this.props; + const { visible } = this.props this.backends.forEach((backend) => { // Instantiate the handler - backend.handler = new backend.initializer(); + backend.handler = new backend.initializer() // Subscribe to index updates // Save unsubscriber backend.unsubscribe = backend.handler.index((index) => { // Only on first download if (!this.state[backend.state]) { - this.onAfterUpdate(backend, true); + this.onAfterUpdate(backend, true) } // Save data into state - this.setState({ [backend.state]: index }); + this.setState({ [backend.state]: index }) // Once data has been fetched, schedule next data update if (visible) { - this.scheduleNextUpdate(backend); + this.scheduleNextUpdate(backend) } - }); - }); + }) + }) } // Handle scheduled updates depending if user has the tab active or not componentDidUpdate = async (prevProps, prevState) => { - const { visible } = this.props; + const { visible } = this.props if (visible !== prevProps.visible) { if (visible) { - console.log("Update backends if needed"); - await this.updateAllIfNeeded(); + console.log('Update backends if needed') + await this.updateAllIfNeeded() - console.log("Schedule next indexes updates"); - this.backends.forEach(this.scheduleNextUpdate); + console.log('Schedule next indexes updates') + this.backends.forEach(this.scheduleNextUpdate) } else { - console.log("Cancel schedule of next indexes updates"); - this.backends.forEach(({ handler }) => handler.cancelUpdateSchedule()); + console.log('Cancel schedule of next indexes updates') + this.backends.forEach(({ handler }) => handler.cancelUpdateSchedule()) } } - }; + } // Cleanup side effects - componentWillUnmount() { + componentWillUnmount () { // Cancel next update timers and update subscriptions this.backends.forEach((backend) => { if (backend.handler) { - backend.handler.cancelUpdateSchedule(); - backend.unsubscribe(); + backend.handler.cancelUpdateSchedule() + backend.unsubscribe() } - }); + }) } - render() { + render () { // Get an array with all handled indexes values const indexes = this.backends .map(({ state }) => state) - .map((state) => this.state[state]); + .map((state) => this.state[state]) const provided = this.backends.reduce( (acc, { name, handler }) => ({ ...acc, - [name]: handler, + [name]: handler }), {} - ); + ) // Show while some index has not yet been loaded - return indexes.some((index) => !index) ? ( - - ) : ( - {this.props.children} - ); + return indexes.some((index) => !index) + ? ( + + ) + : ( + {this.props.children} + ) } } IndexesHandler.defaultProps = { // Retry every 5 minuts after each "not updated yet" response - milllisToNextUpdate: 300_000, -}; + milllisToNextUpdate: 300_000 +} IndexesHandler.propTypes = { // Delay before retrying a schedule after a "not updated yet" response - milllisToNextUpdate: PropTypes.number, -}; + milllisToNextUpdate: PropTypes.number +} // withMapsDataHandler: Add `mapsDataHandler` prop to use maps backend data // withChartsDataHandler: Add `chartsDataHandler` prop to use charts backend data @@ -208,4 +210,4 @@ export default withMapsDataHandler( withChartsDataHandler( withBcnDataHandler(withDocumentVisibility(IndexesHandler)) ) -); +) diff --git a/app/src/Backend/Maps/handler.js b/app/src/Backend/Maps/handler.js index b261bf5a..d17ead0d 100644 --- a/app/src/Backend/Maps/handler.js +++ b/app/src/Backend/Maps/handler.js @@ -1,48 +1,48 @@ -import GHPages from "../Base/GHPages"; -import cache from "../Base/Cache"; +import GHPages from '../Base/GHPages' +import cache from '../Base/Cache' -import MapDataStatic from "./MapDataStatic"; +import MapDataStatic from './MapDataStatic' // Handle data backend and cache for Maps // This is not a singleton: unique error handlers class MapDataHandler extends GHPages { // Visible backend name - name = "Maps JSON Files"; + name = 'Maps JSON Files' // Overload "abstract" member - indexUrl = MapDataStatic.days; + indexUrl = MapDataStatic.days // Invalidate all URLs, except index invalidateAll = async () => { // Invalidate each map JSON first for (const kind of MapDataHandler.kinds()) { for (const value of MapDataHandler.values(kind)) { - this.log(`Invalidate ${kind} - ${value}`); - await cache.invalidate(MapDataStatic.kind[kind].values[value]); + this.log(`Invalidate ${kind} - ${value}`) + await cache.invalidate(MapDataStatic.kind[kind].values[value]) } } - }; + } /* Return static data */ - static kinds = () => Object.keys(MapDataStatic.kind); - static values = (kind) => Object.keys(MapDataStatic.kind[kind].values); - static svg = (kind) => MapDataStatic.kind[kind].svg; + static kinds = () => Object.keys(MapDataStatic.kind) + static values = (kind) => Object.keys(MapDataStatic.kind[kind].values) + static svg = (kind) => MapDataStatic.kind[kind].svg - static metadata = (values, meta) => MapDataStatic.metadata[values][meta]; - static metaColors = (values) => MapDataHandler.metadata(values, "colors"); - static metaTitle = (values) => MapDataHandler.metadata(values, "title"); - static metaLabel = (values) => MapDataHandler.metadata(values, "label"); - static metaName = (values) => MapDataHandler.metadata(values, "name"); + static metadata = (values, meta) => MapDataStatic.metadata[values][meta] + static metaColors = (values) => MapDataHandler.metadata(values, 'colors') + static metaTitle = (values) => MapDataHandler.metadata(values, 'title') + static metaLabel = (values) => MapDataHandler.metadata(values, 'label') + static metaName = (values) => MapDataHandler.metadata(values, 'name') /* Return dynamic data (with cache) */ - days = (callback) => cache.fetch(MapDataStatic.days, callback); - index = (callback) => this.days(callback); + days = (callback) => cache.fetch(MapDataStatic.days, callback) + index = (callback) => this.days(callback) data = (kind, values, callback) => - cache.fetch(MapDataStatic.kind[kind].values[values], callback); + cache.fetch(MapDataStatic.kind[kind].values[values], callback) } -export default MapDataHandler; +export default MapDataHandler diff --git a/app/src/Backend/Maps/index.test.js b/app/src/Backend/Maps/index.test.js index 9aa58e0a..c6bc435c 100644 --- a/app/src/Backend/Maps/index.test.js +++ b/app/src/Backend/Maps/index.test.js @@ -1,6 +1,6 @@ -import MapDataHandler, { context } from "./index"; +import MapDataHandler, { context } from './index' -test("Maps", async () => { - expect(MapDataHandler).toBeTruthy(); - expect(context).toBeTruthy(); -}); +test('Maps', async () => { + expect(MapDataHandler).toBeTruthy() + expect(context).toBeTruthy() +}) diff --git a/app/src/Dashboard.jsx b/app/src/Dashboard.jsx index 38a48ff6..47bad885 100644 --- a/app/src/Dashboard.jsx +++ b/app/src/Dashboard.jsx @@ -22,7 +22,10 @@ import NotificationsIcon from '@material-ui/icons/Notifications'; import Menu from './Menu' import ModalRouterWithRoutes from './ModalRouterWithRoutes' import AppThemeProvider from './AppThemeProvider' -import { withServiceWorkerUpdater, LocalStoragePersistenceService } from '@3m1/service-worker-updater' +import { + withServiceWorkerUpdater, + LocalStoragePersistenceService +} from '@3m1/service-worker-updater' const Copyright = translate('Copyright')((props) => { const { t } = props @@ -163,8 +166,8 @@ const Dashboard = (props) => { } export default translate('Widget')( - withServiceWorkerUpdater( - Dashboard, - { persistenceService: new LocalStoragePersistenceService('CovidRefactored') } - )) + withServiceWorkerUpdater(Dashboard, { + persistenceService: new LocalStoragePersistenceService('CovidRefactored') + }) +) export { Copyright } diff --git a/app/src/ErrorCatcher.jsx b/app/src/ErrorCatcher.jsx index 50d8f381..86f6e68e 100644 --- a/app/src/ErrorCatcher.jsx +++ b/app/src/ErrorCatcher.jsx @@ -1,37 +1,39 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faSyncAlt as faRefresh } from "@fortawesome/free-solid-svg-icons"; -import { translate } from "react-translate"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faSyncAlt as faRefresh } from '@fortawesome/free-solid-svg-icons' +import { translate } from 'react-translate' const ShowErrorDefault = (props) => { - const { onRetry, reloadOnRetry, errorMessage, errorStack, count, t } = props; + const { onRetry, reloadOnRetry, errorMessage, errorStack, count, t } = props return ( <> -

{t("Something went wrong :(")}

- -
+

{errorMessage}

- {reloadOnRetry ? null : ( -

- {t("Counter")}: {count} -

- )} -
{errorStack}
+ {reloadOnRetry + ? null + : ( +

+ {t('Counter')}: {count} +

+ )} +
{errorStack}
- ); -}; + ) +} ShowErrorDefault.defaultProps = { - t: (text) => text, -}; + t: (text) => text +} ShowErrorDefault.propTypes = { onRetry: PropTypes.func.isRequired, @@ -39,8 +41,8 @@ ShowErrorDefault.propTypes = { errorMessage: PropTypes.string.isRequired, errorStack: PropTypes.string.isRequired, count: PropTypes.number.isRequired, - t: PropTypes.func.isRequired, -}; + t: PropTypes.func.isRequired +} // Catches unhandled errors: // - Show it to user @@ -48,71 +50,71 @@ ShowErrorDefault.propTypes = { class ErrorCatcher extends React.PureComponent { initialState = { hasError: false, - errorMessage: "", - errorStack: "", - info: "", - }; + errorMessage: '', + errorStack: '', + info: '' + } - constructor(props) { - super(props); + constructor (props) { + super(props) this.state = { ...this.initialState, - count: 0, - }; + count: 0 + } } - static getDerivedStateFromError(error) { + static getDerivedStateFromError (error) { // Update state so the next render will show the fallback UI. return { hasError: true, errorMessage: error.message, - errorStack: error.stack, - }; + errorStack: error.stack + } } // Catch unhandled errors and send them to help improving the app - componentDidCatch(error, info) { + componentDidCatch (error, info) { this.setState( ({ count }) => ({ info: info.componentStack, - count: count + 1, + count: count + 1 }), - () => this.sendEvent("Unhandled Error") - ); + () => this.sendEvent('Unhandled Error') + ) } reloadApp = () => { - this.sendEvent("Reload page from Error"); + this.sendEvent('Reload page from Error') // Reload app - window.location.reload(); - }; + window.location.reload() + } retry = () => { if (this.props.reloadOnRetry) { - this.reloadApp(); + this.reloadApp() } else { - this.sendEvent("Reload component from Error"); - this.props.onRetry(this.state.count); + this.sendEvent('Reload component from Error') + this.props.onRetry(this.state.count) this.setState({ - ...this.initialState, - }); + ...this.initialState + }) } - }; + } // Send event to GA sendEvent = (event) => { const { props: { sendEvent, origin }, - state: { errorMessage, count }, - } = this; - sendEvent(event, origin, `${errorMessage} (${count})`); - }; + state: { errorMessage, count } + } = this + sendEvent(event, origin, `${errorMessage} (${count})`) + } - render() { + render () { if (this.state.hasError) { - const { reloadOnRetry, ShowError } = this.props; - const { errorMessage, errorStack, count } = this.state; + const { reloadOnRetry, ShowError } = this.props + const { errorMessage, errorStack, count } = this.state // Show error to user return ( @@ -123,33 +125,33 @@ class ErrorCatcher extends React.PureComponent { errorStack={errorStack} count={count} /> - ); + ) } // Execute contained components - return this.props.children; + return this.props.children } } ErrorCatcher.defaultProps = { - origin: "No name", + origin: 'No name', sendEvent: () => {}, onRetry: () => {}, reloadOnRetry: true, - ShowError: translate("ErrorCatcher")(ShowErrorDefault), -}; + ShowError: translate('ErrorCatcher')(ShowErrorDefault) +} ErrorCatcher.propTypes = { origin: PropTypes.string.isRequired, sendEvent: PropTypes.func.isRequired, onRetry: PropTypes.func.isRequired, reloadOnRetry: PropTypes.bool.isRequired, - ShowError: PropTypes.elementType.isRequired, -}; + ShowError: PropTypes.elementType.isRequired +} const withErrorCatcher = (origin, component) => ( {component} -); +) -export default ErrorCatcher; -export { withErrorCatcher, ShowErrorDefault }; +export default ErrorCatcher +export { withErrorCatcher, ShowErrorDefault } diff --git a/app/src/ModalRouter.jsx b/app/src/ModalRouter.jsx index bcaa5862..867faac3 100644 --- a/app/src/ModalRouter.jsx +++ b/app/src/ModalRouter.jsx @@ -1,79 +1,79 @@ -import React from "react"; -import { Route, Switch, Redirect } from "react-router-dom"; +import React from 'react' +import { Route, Switch, Redirect } from 'react-router-dom' -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import Button from "@material-ui/core/Button"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import { useTheme } from "@material-ui/core/styles"; +import Dialog from '@material-ui/core/Dialog' +import DialogActions from '@material-ui/core/DialogActions' +import DialogContent from '@material-ui/core/DialogContent' +import Button from '@material-ui/core/Button' +import useMediaQuery from '@material-ui/core/useMediaQuery' +import { useTheme } from '@material-ui/core/styles' -import { translate } from "react-translate"; +import { translate } from 'react-translate' class CloseModal extends React.PureComponent { - constructor(props) { - super(props); + constructor (props) { + super(props) - if (!("history" in props) || props.history.location.hash !== "") { - global.setTimeout(() => props.history.push("#"), 10); + if (!('history' in props) || props.history.location.hash !== '') { + global.setTimeout(() => props.history.push('#'), 10) } } - render() { - return null; + render () { + return null } } class ModalRouterInner extends React.PureComponent { - constructor(props) { - super(); + constructor (props) { + super() // Set initial state - this.history = props.history; + this.history = props.history this.state = { ...this.getPathState(props.location), autoForce: false, forced: false, - mounted: false, - }; + mounted: false + } } - getPathState(location) { - const path = location.hash.replace(/[^#]*#(.*)$/, "$1"); + getPathState (location) { + const path = location.hash.replace(/[^#]*#(.*)$/, '$1') return { - modalIsOpen: !!path.length && path !== "close", + modalIsOpen: !!path.length && path !== 'close', path, - initialPath: this.state ? this.state.initialPath : path, - }; + initialPath: this.state ? this.state.initialPath : path + } } - componentDidMount() { + componentDidMount () { // Register history change event listener - this.unlisten = this.history.listen(this.handleHistoryChange); + this.unlisten = this.history.listen(this.handleHistoryChange) this.setState({ ...this.getPathState(this.props.location), - mounted: true, - }); + mounted: true + }) } - componentWillUnmount() { + componentWillUnmount () { // Unregister history change event listener - this.unlisten(); + this.unlisten() if (this.timer) { - global.clearTimeout(this.timer); + global.clearTimeout(this.timer) } } // Force user to different URLs - componentDidUpdate(prevProps, prevState) { + componentDidUpdate (prevProps, prevState) { // Remember old URL when forcing if (this.props.force !== false && this.state.forced !== this.props.force) { - this.setState({ forced: this.props.force }); + this.setState({ forced: this.props.force }) } // Clear autoForce after it has been forced if (this.state.autoForce === this.state.path) { - this.setState({ autoForce: false }); + this.setState({ autoForce: false }) } // Force old modal after forcing another (do auto force) @@ -84,45 +84,45 @@ class ModalRouterInner extends React.PureComponent { this.state.initialPath.length > 1 && this.state.autoForce !== this.state.initialPath ) { - this.setState({ autoForce: this.state.initialPath }); + this.setState({ autoForce: this.state.initialPath }) } } - openModal = () => this.setState({ modalIsOpen: true }); + openModal = () => this.setState({ modalIsOpen: true }) closeModal = (propsClose = {}) => { - if (!("history" in propsClose) || propsClose.history.location.path !== "") { + if (!('history' in propsClose) || propsClose.history.location.path !== '') { if (!this.timer) { this.timer = global.setTimeout(() => { - this.history.push("#"); - this.timer = undefined; - }, 10); + this.history.push('#') + this.timer = undefined + }, 10) } } - }; + } handleHistoryChange = (location, action) => { - const state = this.getPathState(location); - this.setState(state); - }; + const state = this.getPathState(location) + this.setState(state) + } - render() { - const { children, initializing, force, fullScreen, t } = this.props; - const { mounted, autoForce, path, forced, modalIsOpen } = this.state; + render () { + const { children, initializing, force, fullScreen, t } = this.props + const { mounted, autoForce, path, forced, modalIsOpen } = this.state if (initializing || !mounted) { - return null; + return null } // Redirect to forced URL if (force !== false && force !== path && force !== forced) { - return ; + return } // If previously have been forced when showing a modal, // force to go back there once the forced has been visited if (autoForce !== false && autoForce !== path) { - return ; + return } return ( @@ -131,8 +131,8 @@ class ModalRouterInner extends React.PureComponent { onClose={this.closeModal} /* Be fullsreen on `sm` sreens */ fullScreen={fullScreen} - aria-labelledby={"modal_heading"} - aria-describedby={"modal_description"} + aria-labelledby='modal_heading' + aria-describedby='modal_description' > @@ -143,38 +143,38 @@ class ModalRouterInner extends React.PureComponent { - - ); + ) } } // Get fullScreen prop using MediaQuery const withFullScreen = (Component) => { return (props) => { - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - return ; - }; -}; + const theme = useTheme() + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) + return + } +} -const ModalRouterInnerWithClasses = withFullScreen(ModalRouterInner); +const ModalRouterInnerWithClasses = withFullScreen(ModalRouterInner) class ModalRouter extends React.PureComponent { - render() { - const { children, ...rest } = this.props; + render () { + const { children, ...rest } = this.props return ( ( )} /> - ); + ) } } -export default translate("ModalRouter")(ModalRouter); +export default translate('ModalRouter')(ModalRouter) diff --git a/app/src/ModalRouterWithRoutes.jsx b/app/src/ModalRouterWithRoutes.jsx index dde3379a..2109026c 100644 --- a/app/src/ModalRouterWithRoutes.jsx +++ b/app/src/ModalRouterWithRoutes.jsx @@ -22,7 +22,10 @@ const ModalRouterWithRoutes = (props) => { render={() => withErrorCatcher( 'Language', - + )} /> { if (this.timer !== false) { - clearTimeout(this.timer); - this.timer = false; + clearTimeout(this.timer) + this.timer = false } - }; + } // Run function, if not throtled or if force == true - run(force, timeout, func) { + run (force, timeout, func) { if (force) { - this.clear(); + this.clear() } if (this.timer === false) { - this.start(timeout); - func(); + this.start(timeout) + func() } } } -export default Throtle; +export default Throtle diff --git a/app/src/Widget/Actions.jsx b/app/src/Widget/Actions.jsx index d57e019c..1c4ae257 100644 --- a/app/src/Widget/Actions.jsx +++ b/app/src/Widget/Actions.jsx @@ -1,46 +1,46 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { translate } from "react-translate"; +import { translate } from 'react-translate' -import Button from "@material-ui/core/Button"; -import Dialog from "@material-ui/core/Dialog"; -import DialogActions from "@material-ui/core/DialogActions"; -import DialogContent from "@material-ui/core/DialogContent"; -import DialogTitle from "@material-ui/core/DialogTitle"; -import useMediaQuery from "@material-ui/core/useMediaQuery"; -import { useTheme } from "@material-ui/core/styles"; +import Button from '@material-ui/core/Button' +import Dialog from '@material-ui/core/Dialog' +import DialogActions from '@material-ui/core/DialogActions' +import DialogContent from '@material-ui/core/DialogContent' +import DialogTitle from '@material-ui/core/DialogTitle' +import useMediaQuery from '@material-ui/core/useMediaQuery' +import { useTheme } from '@material-ui/core/styles' -import Draggable from "react-draggable"; +import Draggable from 'react-draggable' -import WidgetMenu from "./Menu"; -import ErrorCatcher from "../ErrorCatcher"; +import WidgetMenu from './Menu' +import ErrorCatcher from '../ErrorCatcher' class DraggableResponsiveDialogUntranslated extends React.PureComponent { /* Handle dialog open/close status */ state = { - open: false, - }; + open: false + } // Set open status to the selected menu code handleClickOpen = (open) => { - this.setState({ open }); - }; + this.setState({ open }) + } handleClose = () => { - this.setState({ open: false }); - }; + this.setState({ open: false }) + } - render() { - const { open } = this.state; + render () { + const { open } = this.state // Get `restProps` for child renders (including `id`) - const { sections, fullScreen, t, ...restProps } = this.props; - const { id } = this.props; + const { sections, fullScreen, t, ...restProps } = this.props + const { id } = this.props // Get shortcut to content & title render functions - const Content = open ? sections[open].render : () => {}; - const Title = open ? sections[open].title : () => {}; + const Content = open ? sections[open].render : () => {} + const Title = open ? sections[open].title : () => {} return ( <> @@ -69,40 +69,40 @@ class DraggableResponsiveDialogUntranslated extends React.PureComponent { <DialogContent> <ErrorCatcher reloadOnRetry={false} - origin={`${t("Widget")} ${open} ${Title({ ...restProps, t })}`} + origin={`${t('Widget')} ${open} ${Title({ ...restProps, t })}`} > <Content {...restProps} t={t} /> </ErrorCatcher> </DialogContent> <DialogActions> - <Button autoFocus onClick={this.handleClose} color="primary"> - {t("Close")} + <Button autoFocus onClick={this.handleClose} color='primary'> + {t('Close')} </Button> </DialogActions> </Dialog> ) : null} </> - ); + ) } } -const DraggableResponsiveDialog = translate("Widget")( +const DraggableResponsiveDialog = translate('Widget')( DraggableResponsiveDialogUntranslated -); +) DraggableResponsiveDialog.propTypes = { id: PropTypes.string.isRequired, // [sectionID('view','edit','legend',...)]: {icon: string, label: string/fn } - sections: PropTypes.object.isRequired, -}; + sections: PropTypes.object.isRequired +} // Get fullScreen prop using MediaQuery const withFullScreen = (Component) => { return (props) => { - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); - return <Component {...props} fullScreen={fullScreen} />; - }; -}; + const theme = useTheme() + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) + return <Component {...props} fullScreen={fullScreen} /> + } +} -export default withFullScreen(DraggableResponsiveDialog); +export default withFullScreen(DraggableResponsiveDialog) diff --git a/app/src/Widget/List.jsx b/app/src/Widget/List.jsx index 7f447796..3fa5a092 100644 --- a/app/src/Widget/List.jsx +++ b/app/src/Widget/List.jsx @@ -1,35 +1,35 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import ListHeader from "./ListHeader"; -import MenuAddWidget from "./MenuAddWidget"; -import WidgetsTypes from "./Widgets"; -import SortableWidgetContainer from "./SortableWidgetContainer"; +import ListHeader from './ListHeader' +import MenuAddWidget from './MenuAddWidget' +import WidgetsTypes from './Widgets' +import SortableWidgetContainer from './SortableWidgetContainer' -import Throtle from "../Throtle"; -import DateSlider from "./Slider"; -import Loading from "../Loading"; +import Throtle from '../Throtle' +import DateSlider from './Slider' +import Loading from '../Loading' -import { withIndex as withMapsIndex } from "../Backend/Maps/context"; -import { WidgetStorageContextProvider, withStorageHandler } from "./Storage"; +import { withIndex as withMapsIndex } from '../Backend/Maps/context' +import { WidgetStorageContextProvider, withStorageHandler } from './Storage' // GUID generator: used to create unique temporal IDs for widgets const S4 = () => - (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) const guidGenerator = () => - "a-" + + 'a-' + S4() + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + - "-" + + '-' + S4() + S4() + - S4(); + S4() // Renders widgets list // - Manages days list (using backend) and currently selected Date with a Slider @@ -46,80 +46,80 @@ class WidgetsList extends React.PureComponent { // Managed data: days & currently selected day state = { currentDate: null, - widgets: [], - }; + widgets: [] + } // Widgets list temporal IDs // Used to maintain widget uniqueness during a session - widgetsIds = []; + widgetsIds = [] // Used in the slider to prevent blocking the UI // with excessive calls on mouse move - throtle = new Throtle(); + throtle = new Throtle() updateWidgets = () => { // Ensure a unique and constant ID for each widget - const length = this.widgetsIds.length; + const length = this.widgetsIds.length if (length < this.props.widgets.length) { this.widgetsIds = this.props.widgets.map((w, index) => index < length ? this.widgetsIds[index] : guidGenerator() - ); + ) } // Generate a cached (in state) madurated list of widgets const widgets = this.props.widgets.map(({ type, payload }, index) => ({ id: this.widgetsIds[index], Component: WidgetsTypes.find((w) => w.key === type).Component, - ...{ payload }, - })); + ...{ payload } + })) - this.setState({ widgets }); - }; + this.setState({ widgets }) + } - componentDidMount() { - const { days } = this.props; - this.updateWidgets(); - this.setState({ currentDate: days.length - 1 }); + componentDidMount () { + const { days } = this.props + this.updateWidgets() + this.setState({ currentDate: days.length - 1 }) } // Cleanup side effects - componentWillUnmount() { + componentWillUnmount () { // Cancel possible throtle timer - this.throtle.clear(); + this.throtle.clear() } - componentDidUpdate(prevProps, prevState) { - const { days: daysOld } = prevProps; - const { days } = this.props; + componentDidUpdate (prevProps, prevState) { + const { days: daysOld } = prevProps + const { days } = this.props if (daysOld !== days) { - const { currentDate } = this.state; + const { currentDate } = this.state const isLast = - !currentDate || !daysOld || currentDate === daysOld.length - 1; + !currentDate || !daysOld || currentDate === daysOld.length - 1 // If we were on last `days` item, go to the new last if (isLast) { this.setState({ - currentDate: days.length - 1, - }); + currentDate: days.length - 1 + }) } } if (prevProps.widgets !== this.props.widgets) { - this.updateWidgets(); + this.updateWidgets() } } // Slider helpers onSetDate = (event, currentDate) => - this.throtle.run(false, 10, () => this.setState({ currentDate })); + this.throtle.run(false, 10, () => this.setState({ currentDate })) // Adds a new default widget to the list onAdd = (widgetType) => { - const widgets = [...this.props.widgets]; - widgets.push({ type: widgetType ?? "map" }); - return this.props.onChangeData({ widgets }); - }; + const widgets = [...this.props.widgets] + widgets.push({ type: widgetType ?? 'map' }) + return this.props.onChangeData({ widgets }) + } // Handle onRemove event from widget // Uses `this.widgetsIDs` for widget identification @@ -127,47 +127,47 @@ class WidgetsList extends React.PureComponent { onRemove = (id) => { const widgets = this.props.widgets.filter( (w, index) => this.widgetsIds[index] !== id - ); - this.widgetsIds = this.widgetsIds.filter((idElement) => idElement !== id); - return this.props.onChangeData({ widgets }); - }; + ) + this.widgetsIds = this.widgetsIds.filter((idElement) => idElement !== id) + return this.props.onChangeData({ widgets }) + } // Handle onChangeData event from widget // Calls parent' onChangeData to save the data onChangeData = (id, data) => { - const { widgets } = this.props; + const { widgets } = this.props const widgetsNew = widgets.map((w, i) => ({ ...w, - ...(id === this.widgetsIds[i] ? { payload: data } : {}), - })); - return this.props.onChangeData({ widgets: widgetsNew }); - }; + ...(id === this.widgetsIds[i] ? { payload: data } : {}) + })) + return this.props.onChangeData({ widgets: widgetsNew }) + } // Handle widgets reordering // Takes care of both `this.widgetsIds` and `this.props.widgets` onReorder = (oldIndex, newIndex) => { // Reorder ID - const id = this.widgetsIds[oldIndex]; - this.widgetsIds.splice(oldIndex, 1); - this.widgetsIds.splice(newIndex, 0, id); + const id = this.widgetsIds[oldIndex] + this.widgetsIds.splice(oldIndex, 1) + this.widgetsIds.splice(newIndex, 0, id) // Reorder data (keep props immutable) // Use `widgets.filter` to clone original array - const { widgets } = this.props; - const widget = widgets[oldIndex]; - const widgetsNew = widgets.filter((w, index) => index !== oldIndex); - widgetsNew.splice(newIndex, 0, widget); + const { widgets } = this.props + const widget = widgets[oldIndex] + const widgetsNew = widgets.filter((w, index) => index !== oldIndex) + widgetsNew.splice(newIndex, 0, widget) // Save data - return this.props.onChangeData({ widgets: widgetsNew }); - }; + return this.props.onChangeData({ widgets: widgetsNew }) + } - render() { - const { days } = this.props; - const { widgets, currentDate } = this.state; + render () { + const { days } = this.props + const { widgets, currentDate } = this.state if (currentDate === null) { - return <Loading />; + return <Loading /> } return ( @@ -194,19 +194,19 @@ class WidgetsList extends React.PureComponent { widgets={widgets} /> </> - ); + ) } } WidgetsList.propTypes = { widgets: PropTypes.array.isRequired, days: PropTypes.array.isRequired, - onChangeData: PropTypes.func.isRequired, -}; + onChangeData: PropTypes.func.isRequired +} // withStorageHandler: Handle params from storage providers (route + localStorage) into props // withMapsIndex: Add `days` prop to use maps backend index (days) -const WidgetsListWithHOCs = withStorageHandler(withMapsIndex(WidgetsList)); +const WidgetsListWithHOCs = withStorageHandler(withMapsIndex(WidgetsList)) // Manage some context providers details: // - pathFilter: How to split `location` (<Router> `path` prop) @@ -214,22 +214,22 @@ const WidgetsListWithHOCs = withStorageHandler(withMapsIndex(WidgetsList)); // - paramsToString: Parse back a JS object into a `location` path const WidgetsListWithStorageContextProviders = (props) => ( <WidgetStorageContextProvider - pathFilter={"/:widgets*"} + pathFilter='/:widgets*' paramsFilter={(params) => { - const { widgets } = params; - let widgetsParsed; + const { widgets } = params + let widgetsParsed try { widgetsParsed = widgets - .split("/") - .map((w) => JSON.parse(decodeURIComponent(w))); + .split('/') + .map((w) => JSON.parse(decodeURIComponent(w))) } catch (err) { - widgetsParsed = []; + widgetsParsed = [] } return Object.assign({}, props, { - widgets: widgetsParsed, - }); + widgets: widgetsParsed + }) }} paramsToString={(params) => { const widgets = params.widgets @@ -237,16 +237,16 @@ const WidgetsListWithStorageContextProviders = (props) => ( encodeURIComponent( JSON.stringify({ type: w.type, - payload: w.payload, + payload: w.payload }) ) ) - .join("/"); - return `/${widgets}`; + .join('/') + return `/${widgets}` }} > <WidgetsListWithHOCs {...props} /> </WidgetStorageContextProvider> -); +) -export default WidgetsListWithStorageContextProviders; +export default WidgetsListWithStorageContextProviders diff --git a/app/src/Widget/MenuAddWidget.jsx b/app/src/Widget/MenuAddWidget.jsx index 292675db..7b145434 100644 --- a/app/src/Widget/MenuAddWidget.jsx +++ b/app/src/Widget/MenuAddWidget.jsx @@ -53,34 +53,36 @@ const MenuAddWidget = React.memo((props) => { <AddIcon /> </Fab> </Tooltip> - {anchorEl ? ( - <Menu - id='widget-add-menu' - anchorEl={anchorEl} - keepMounted - open={open} - onClose={handleClose} - transformOrigin={{ - vertical: 'center', - horizontal: 'right' - }} - PaperProps={{ - style: { - width: '30ch' - } - }} - > - {options.map((option) => ( - <WidgetMenuItem - key={option.key} - option={option.key} - onClick={handleClickElement} - icon={option.icon} - label={t(option.name)} - /> - ))} - </Menu> - ) : null} + {anchorEl + ? ( + <Menu + id='widget-add-menu' + anchorEl={anchorEl} + keepMounted + open={open} + onClose={handleClose} + transformOrigin={{ + vertical: 'center', + horizontal: 'right' + }} + PaperProps={{ + style: { + width: '30ch' + } + }} + > + {options.map((option) => ( + <WidgetMenuItem + key={option.key} + option={option.key} + onClick={handleClickElement} + icon={option.icon} + label={t(option.name)} + /> + ))} + </Menu> + ) + : null} </> ) }) diff --git a/app/src/Widget/MenuItem.jsx b/app/src/Widget/MenuItem.jsx index fbc2802f..204587c4 100644 --- a/app/src/Widget/MenuItem.jsx +++ b/app/src/Widget/MenuItem.jsx @@ -1,29 +1,29 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import MenuItem from "@material-ui/core/MenuItem"; -import ListItemIcon from "@material-ui/core/ListItemIcon"; -import ListItemText from "@material-ui/core/ListItemText"; +import MenuItem from '@material-ui/core/MenuItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ListItemText from '@material-ui/core/ListItemText' // Renders an item into the widget's popup actions menu // Ensures click event uses widget id const WidgetMenuItem = React.forwardRef((props, ref) => { - const { onClick, option, icon, label } = props; - const handleClick = () => onClick(option); + const { onClick, option, icon, label } = props + const handleClick = () => onClick(option) return ( <MenuItem key={option} onClick={handleClick} ref={ref}> <ListItemIcon>{icon}</ListItemIcon> <ListItemText primary={label || option} /> </MenuItem> - ); -}); + ) +}) -WidgetMenuItem.displayName = WidgetMenuItem; +WidgetMenuItem.displayName = WidgetMenuItem WidgetMenuItem.propTypes = { icon: PropTypes.element.isRequired, label: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired, - option: PropTypes.string.isRequired, -}; + option: PropTypes.string.isRequired +} -export default WidgetMenuItem; +export default WidgetMenuItem diff --git a/app/src/Widget/Storage/StorageContextProviderLocalStorage.jsx b/app/src/Widget/Storage/StorageContextProviderLocalStorage.jsx index b7b66c78..d9456f22 100644 --- a/app/src/Widget/Storage/StorageContextProviderLocalStorage.jsx +++ b/app/src/Widget/Storage/StorageContextProviderLocalStorage.jsx @@ -1,9 +1,9 @@ -import React from "react"; +import React from 'react' -import Storage from "react-simple-storage"; +import Storage from 'react-simple-storage' -import Loading from "../../Loading"; -import WidgetStorageContext from "./StorageContext"; +import Loading from '../../Loading' +import WidgetStorageContext from './StorageContext' /* Context provider using localStorage as data source for Widgets params @@ -11,49 +11,51 @@ import WidgetStorageContext from "./StorageContext"; class LocalStorageProvider extends React.Component { state = { initializing: true, - data: {}, - }; + data: {} + } onChangeData = (data) => { - this.setState({ data }); - }; + this.setState({ data }) + } stopInitializing = () => { - this.setState({ initializing: false }); - }; + this.setState({ initializing: false }) + } - render() { - const { children, ...props } = this.props; - const { initializing, data } = this.state; + render () { + const { children, ...props } = this.props + const { initializing, data } = this.state return ( <> {/* Persistent state saver into localStorage, only on window close */} <Storage parent={this} - prefix="LocalStorageProvider" - blacklist={["initializing"]} + prefix='LocalStorageProvider' + blacklist={['initializing']} onParentStateHydrated={this.stopInitializing} /> - {initializing ? ( - <Loading /> - ) : ( - <WidgetStorageContext.Provider - value={{ - onChangeData: this.onChangeData, - ...props, - data, - }} - > - {children} - </WidgetStorageContext.Provider> - )} + {initializing + ? ( + <Loading /> + ) + : ( + <WidgetStorageContext.Provider + value={{ + onChangeData: this.onChangeData, + ...props, + data + }} + > + {children} + </WidgetStorageContext.Provider> + )} </> - ); + ) } } -LocalStorageProvider.propTypes = {}; +LocalStorageProvider.propTypes = {} -export default LocalStorageProvider; +export default LocalStorageProvider diff --git a/app/src/Widget/Storage/StorageContextProviderRouter.jsx b/app/src/Widget/Storage/StorageContextProviderRouter.jsx index adf9140c..c0c5f5a8 100644 --- a/app/src/Widget/Storage/StorageContextProviderRouter.jsx +++ b/app/src/Widget/Storage/StorageContextProviderRouter.jsx @@ -1,68 +1,67 @@ -import React from "react"; -import { PropTypes } from "prop-types"; -import { Route, Switch } from "react-router-dom"; +import React from 'react' +import { PropTypes } from 'prop-types' +import { Route, Switch } from 'react-router-dom' -import WidgetStorageContext from "./StorageContext"; -import withStorageHandler from "./withStorageHandler"; +import WidgetStorageContext from './StorageContext' +import withStorageHandler from './withStorageHandler' /* Context provider using React Route as data source for Widgets params */ class RouterProvider extends React.Component { // History change listener - unlisten = () => {}; + unlisten = () => {} - constructor(props) { - super(props); - this.history = props.history; + constructor (props) { + super(props) + this.history = props.history } historyPush = (data, replace = false) => { - const newPath = this.props.paramsToString(data); + const newPath = this.props.paramsToString(data) // Only PUSH or REPLACE if something have to change if (this.history.location.pathname !== newPath) { if (!replace) { - this.history.push(newPath + this.history.location.hash); + this.history.push(newPath + this.history.location.hash) } else { - this.history.replace(newPath + this.history.location.hash); + this.history.replace(newPath + this.history.location.hash) } } - }; + } handleHistoryChange = (location, action) => { // Do nothing when change is made by us - if (action !== "POP") { - return; + if (action !== 'POP') { } - }; + } componentDidMount = () => { // Register history change event listener - this.unlisten = this.history.listen(this.handleHistoryChange); + this.unlisten = this.history.listen(this.handleHistoryChange) // Check if data from localStorage is better if ( !this.props.params.widgets.length && this.props.data.widgets && this.props.data.widgets.length ) { - this.historyPush(this.props.data, true); + this.historyPush(this.props.data, true) } - }; + } componentWillUnmount = () => { // Unregister history change event listener - this.unlisten(); - }; + this.unlisten() + } // Save data into location onChangeData = (data) => { - this.historyPush({ ...data }, true); - this.props.onChangeData({ ...data }); - }; + this.historyPush({ ...data }, true) + this.props.onChangeData({ ...data }) + } // Render the provider and the children - render() { + render () { const { children, data, @@ -72,7 +71,7 @@ class RouterProvider extends React.Component { history, onChangeData, ...props - } = this.props; + } = this.props return ( <WidgetStorageContext.Provider @@ -80,12 +79,12 @@ class RouterProvider extends React.Component { onChangeData: this.onChangeData, ...props, ...data, - ...params, + ...params }} > {children} </WidgetStorageContext.Provider> - ); + ) } } @@ -95,12 +94,12 @@ RouterProvider.propTypes = { onChangeData: PropTypes.func.isRequired, // Receive params and history from browser location/Router params: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, -}; + history: PropTypes.object.isRequired +} // Proxy Route data into child props provider const RouterProviderWithSwitch = (props) => { - const { pathFilter, paramsFilter, paramsToString, ...restProps } = props; + const { pathFilter, paramsFilter, paramsToString, ...restProps } = props return ( <Switch> <Route @@ -108,33 +107,33 @@ const RouterProviderWithSwitch = (props) => { render={(propsRoute) => { const { match: { params }, - history, - } = propsRoute; - const processedParams = paramsFilter(params); + history + } = propsRoute + const processedParams = paramsFilter(params) return ( <RouterProvider {...{ paramsFilter, paramsToString, history }} {...restProps} params={processedParams} /> - ); + ) }} /> </Switch> - ); -}; + ) +} RouterProviderWithSwitch.propTypes = { // How to handle hash storage pathFilter: PropTypes.string.isRequired, paramsFilter: PropTypes.func.isRequired, - paramsToString: PropTypes.func.isRequired, -}; + paramsToString: PropTypes.func.isRequired +} // Consume localStorage storage provider const RouterProviderWithSwitchWithStorageHandler = withStorageHandler( RouterProviderWithSwitch, - { data: { widgets: { type: "map" } } } // TODO: default -); + { data: { widgets: { type: 'map' } } } // TODO: default +) -export default RouterProviderWithSwitchWithStorageHandler; +export default RouterProviderWithSwitchWithStorageHandler diff --git a/app/src/Widget/Storage/withStorageHandler.jsx b/app/src/Widget/Storage/withStorageHandler.jsx index a0842a0b..e1a042d5 100644 --- a/app/src/Widget/Storage/withStorageHandler.jsx +++ b/app/src/Widget/Storage/withStorageHandler.jsx @@ -1,6 +1,6 @@ -import React from "react"; +import React from 'react' -import WidgetStorageContext from "./StorageContext"; +import WidgetStorageContext from './StorageContext' /* Proxies data from this context into Component props @@ -8,9 +8,9 @@ import WidgetStorageContext from "./StorageContext"; const withStorageHandler = (Component, defaults) => { class WithStorageHandler extends React.Component { storageHandler = (data) => { - const { forwardedRef, ...props } = this.props; - const { onChangeData = () => {}, ...restData } = data; - const allData = Object.assign({}, defaults, restData, props); + const { forwardedRef, ...props } = this.props + const { onChangeData = () => {}, ...restData } = data + const allData = Object.assign({}, defaults, restData, props) return ( <Component @@ -19,27 +19,27 @@ const withStorageHandler = (Component, defaults) => { onChangeData={onChangeData} {...allData} /> - ); - }; + ) + } - render() { + render () { return ( <WidgetStorageContext.Consumer> {this.storageHandler} </WidgetStorageContext.Consumer> - ); + ) } } // Return wrapper respecting ref const forwarded = React.forwardRef((props, ref) => ( <WithStorageHandler {...props} forwardedRef={ref} /> - )); + )) - forwarded.propTypes = Component.propTypes; - forwarded.defaultProps = Component.defaultProps; + forwarded.propTypes = Component.propTypes + forwarded.defaultProps = Component.defaultProps - return forwarded; -}; + return forwarded +} -export default withStorageHandler; +export default withStorageHandler diff --git a/app/src/Widget/Widgets/Bcn/EditDataset.jsx b/app/src/Widget/Widgets/Bcn/EditDataset.jsx index 3cf50675..a44aabc1 100644 --- a/app/src/Widget/Widgets/Bcn/EditDataset.jsx +++ b/app/src/Widget/Widgets/Bcn/EditDataset.jsx @@ -1,63 +1,63 @@ -import React from "react"; +import React from 'react' -import { withStyles } from "@material-ui/core/styles"; -import TreeView from "@material-ui/lab/TreeView"; -import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; -import ChevronRightIcon from "@material-ui/icons/ChevronRight"; -import TreeItem from "@material-ui/lab/TreeItem"; +import { withStyles } from '@material-ui/core/styles' +import TreeView from '@material-ui/lab/TreeView' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' +import TreeItem from '@material-ui/lab/TreeItem' -import { withHandler } from "../../../Backend/Bcn/context"; +import { withHandler } from '../../../Backend/Bcn/context' // From: https://material-ui.com/components/tree-view/#rich-object const styles = { root: { flexGrow: 1, - maxWidth: 400, - }, -}; + maxWidth: 400 + } +} const Tree = ({ node }) => ( <TreeItem key={node.code} - nodeId={`${!("values" in node) ? "DISABLED-" : ""}${node.code}`} + nodeId={`${!('values' in node) ? 'DISABLED-' : ''}${node.code}`} label={node.title} > {Array.isArray(node.sections) ? node.sections.map((node, key) => <Tree {...{ node, key }} />) : null} </TreeItem> -); +) class RecursiveTreeView extends React.Component { state = { breadcrumb: [], - value: "", - }; + value: '' + } onNodeSelect = (event, value, ...rest) => { // Don't save values for disabled nodes if (!/^DISABLED-/.test(value)) { - this.props.onChange(value); + this.props.onChange(value) } - }; + } - constructor(props) { - super(props); + constructor (props) { + super(props) - const { value, bcnDataHandler } = props; + const { value, bcnDataHandler } = props this.state = { value, breadcrumb: bcnDataHandler .findBreadcrumb(null, value) - .map((node) => `${!("values" in node) ? "DISABLED-" : ""}${node.code}`), - }; + .map((node) => `${!('values' in node) ? 'DISABLED-' : ''}${node.code}`) + } } render = () => { - const { classes, bcnDataHandler, ...restProps } = this.props; - const { breadcrumb, value } = this.state; + const { classes, bcnDataHandler, ...restProps } = this.props + const { breadcrumb, value } = this.state return ( <TreeView @@ -70,13 +70,13 @@ class RecursiveTreeView extends React.Component { {...restProps} > {bcnDataHandler - .filter((section) => ["graph", "chart"].includes(section.type)) + .filter((section) => ['graph', 'chart'].includes(section.type)) .map((section) => ( <Tree key={section.code} node={section} /> ))} </TreeView> - ); - }; + ) + } } -export default withHandler(withStyles(styles)(RecursiveTreeView)); +export default withHandler(withStyles(styles)(RecursiveTreeView)) diff --git a/app/src/Widget/Widgets/Bcn/Widget.jsx b/app/src/Widget/Widgets/Bcn/Widget.jsx index c3051c04..5cad9185 100644 --- a/app/src/Widget/Widgets/Bcn/Widget.jsx +++ b/app/src/Widget/Widgets/Bcn/Widget.jsx @@ -1,21 +1,21 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import BcnLogo from "./BcnLogo"; -import { faEdit } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import BcnLogo from './BcnLogo' +import { faEdit } from '@fortawesome/free-solid-svg-icons' -import { withHandler, withData } from "../../../Backend/Bcn/context"; +import { withHandler, withData } from '../../../Backend/Bcn/context' -import Chart from "../Common/Chart"; -import Edit from "./Edit"; -import withWidget from "../../Widget"; +import Chart from '../Common/Chart' +import Edit from './Edit' +import withWidget from '../../Widget' const ChartWrapper = withWidget({ // The normal view view: { icon: <BcnLogo />, - label: ({ t }) => t("View"), + label: ({ t }) => t('View'), title: (props) => props.title, render: withData((props) => { const { @@ -31,108 +31,108 @@ const ChartWrapper = withWidget({ bcnDataHandler, // Passed through ...restProps - } = props; + } = props // Once downloaded, set parent's data, so it can properly // set title and other widget components - React.useEffect(() => setBcnData(data), [data, setBcnData]); + React.useEffect(() => setBcnData(data), [data, setBcnData]) - return <Chart {...{ dies, data }} {...restProps} />; - }), + return <Chart {...{ dies, data }} {...restProps} /> + }) }, // Edit data edit: { icon: <FontAwesomeIcon icon={faEdit} />, - label: ({ t }) => t("Edit"), - title: ({ t }) => t("Edit BCN parameters"), - render: Edit, - }, -}); + label: ({ t }) => t('Edit'), + title: ({ t }) => t('Edit BCN parameters'), + render: Edit + } +}) /* Combine BcnData backend with Chart */ class DataHandler extends React.Component { state = { - bcnData: null, - }; + bcnData: null + } - constructor(props) { - super(props); + constructor (props) { + super(props) // Set initial state for meta info this.state = { ...this.state, - ...this.getMeta(this.props.dataset, this.state.bcnData), - }; + ...this.getMeta(this.props.dataset, this.state.bcnData) + } } defaultMeta = { - title: "...", - name: "...", + title: '...', + name: '...', theme: null, yAxis: null, - source: null, - }; + source: null + } - setBcnData = (bcnData) => this.setState({ bcnData }); + setBcnData = (bcnData) => this.setState({ bcnData }) // Get metadata from given params getMeta = (dataset, bcnData) => { if (!bcnData) { - return this.defaultMeta; + return this.defaultMeta } - const found = this.props.bcnDataHandler.findChild(null, dataset); + const found = this.props.bcnDataHandler.findChild(null, dataset) if (!found) { - return this.defaultMeta; + return this.defaultMeta } - const { title, description, theme, yAxis, source } = found; + const { title, description, theme, yAxis, source } = found return { title, name: title, description, theme, yAxis, - source, - }; - }; + source + } + } // Update metadata updateData = () => { const { state: { bcnData }, - props: { dataset }, - } = this; + props: { dataset } + } = this this.setState({ - ...this.getMeta(dataset, bcnData), - }); - }; + ...this.getMeta(dataset, bcnData) + }) + } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate (prevProps, prevState) { const { state: { bcnData }, - props: { dataset }, - } = this; + props: { dataset } + } = this if (dataset !== prevProps.dataset || bcnData !== prevState.bcnData) { - this.updateData(); + this.updateData() } } // Update params in parents onChange = (dataset) => { this.props.onChangeData(this.props.id, { - dataset, - }); - }; + dataset + }) + } // Datasets are on same data object onChangeDataset = (dataset) => { - this.onChange(dataset); - }; + this.onChange(dataset) + } - render() { + render () { const { state: { // Meta @@ -141,20 +141,20 @@ class DataHandler extends React.Component { description, theme, yAxis, - source, + source }, props: { days, indexValues, id, dataset, onRemove }, onChangeDataset, - setBcnData, - } = this; + setBcnData + } = this return ( <div style={{ - minWidth: "200px", - height: "100%", - paddingTop: ".3em", - flex: "1 1 0px", + minWidth: '200px', + height: '100%', + paddingTop: '.3em', + flex: '1 1 0px' }} > <ChartWrapper @@ -170,7 +170,7 @@ class DataHandler extends React.Component { description, theme, yAxis, - source, + source }} // Used in Edit onChangeDataset={onChangeDataset} @@ -179,15 +179,15 @@ class DataHandler extends React.Component { setBcnData={setBcnData} /> </div> - ); + ) } } DataHandler.defaultProps = { - dataset: "IND_DEF_OBS_CAT", + dataset: 'IND_DEF_OBS_CAT', onChangeData: () => {}, - onRemove: () => {}, -}; + onRemove: () => {} +} DataHandler.propTypes = { days: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -195,7 +195,7 @@ DataHandler.propTypes = { id: PropTypes.string.isRequired, onChangeData: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired, - dataset: PropTypes.string.isRequired, -}; + dataset: PropTypes.string.isRequired +} -export default withHandler(DataHandler); +export default withHandler(DataHandler) diff --git a/app/src/Widget/Widgets/Chart/Edit.jsx b/app/src/Widget/Widgets/Chart/Edit.jsx index 5c2e9e12..e8893fc8 100644 --- a/app/src/Widget/Widgets/Chart/Edit.jsx +++ b/app/src/Widget/Widgets/Chart/Edit.jsx @@ -1,40 +1,40 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { translate } from "react-translate"; +import { translate } from 'react-translate' -import { makeStyles } from "@material-ui/core/styles"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import Select from "@material-ui/core/Select"; -import Divider from "@material-ui/core/Divider"; +import { makeStyles } from '@material-ui/core/styles' +import InputLabel from '@material-ui/core/InputLabel' +import FormHelperText from '@material-ui/core/FormHelperText' +import FormControl from '@material-ui/core/FormControl' +import Select from '@material-ui/core/Select' +import Divider from '@material-ui/core/Divider' -import ChartRegionSelector from "./EditRegion"; +import ChartRegionSelector from './EditRegion' // UI-Material Styles const useStyles = makeStyles((theme) => ({ formControl: { - minWidth: 120, + minWidth: 120 }, selectEmpty: { - marginTop: theme.spacing(2), + marginTop: theme.spacing(2) }, labelFixed: { - position: "initial", - transform: "unset", - marginBottom: theme.spacing(2), - }, -})); + position: 'initial', + transform: 'unset', + marginBottom: theme.spacing(2) + } +})) const FormDecorators = (props) => { - const classes = useStyles(); - const { label, help, id, children, fixTree } = props; + const classes = useStyles() + const { label, help, id, children, fixTree } = props return ( <div style={{ - margin: "0 auto", - display: "block", + margin: '0 auto', + display: 'block' }} > <FormControl required className={classes.formControl}> @@ -45,17 +45,19 @@ const FormDecorators = (props) => { {label} </InputLabel> {children} - {help ? ( - <FormHelperText id={`helper-text-${id}`}>{help}</FormHelperText> - ) : null} + {help + ? ( + <FormHelperText id={`helper-text-${id}`}>{help}</FormHelperText> + ) + : null} </FormControl> </div> - ); -}; + ) +} // Renders a Select, with options from props const Selector = (props) => { - const { label, help, id, options, ...restProps } = props; + const { label, help, id, options, ...restProps } = props return ( <FormDecorators id={id} label={label} help={help}> <Select @@ -64,7 +66,7 @@ const Selector = (props) => { aria-describedby={`helper-text-${id}`} inputProps={{ name: label, - id: id, + id }} > {options.map(({ value, label }) => ( @@ -74,78 +76,81 @@ const Selector = (props) => { ))} </Select> </FormDecorators> - ); -}; + ) +} // Renders a Select with chart division options -const ChartDivisionSelector = translate("Widget/Chart/Edit")((props) => { - const { t, divisions, ...restProps } = props; +const ChartDivisionSelector = translate('Widget/Chart/Edit')((props) => { + const { t, divisions, ...restProps } = props const ChartDivisionSelectorOptions = React.useMemo( () => divisions.map((kind) => ({ value: kind, label: kind })), [divisions] - ); + ) return ( <Selector options={ChartDivisionSelectorOptions} - label={t("Division")} - help={t("Select the division type to show")} + label={t('Division')} + help={t('Select the division type to show')} {...restProps} /> - ); -}); + ) +}) // Renders a Select with chart population options -const ChartPopulationSelector = translate("Widget/Chart/Edit")((props) => { - const { t, populations, ...restProps } = props; +const ChartPopulationSelector = translate('Widget/Chart/Edit')((props) => { + const { t, populations, ...restProps } = props const ChartPopulationSelectorOptions = React.useMemo( () => populations.map((kind) => ({ value: kind, label: kind })), [populations] - ); + ) return ( <Selector - label={t("Population")} - help={t("Select the population")} + label={t('Population')} + help={t('Select the population')} options={ChartPopulationSelectorOptions} {...restProps} /> - ); -}); + ) +}) // Renders a Select with chart population options -const ChartDatasetSelector = translate("Widget/Chart/Edit")((props) => { - const { t, ...restProps } = props; +const ChartDatasetSelector = translate('Widget/Chart/Edit')((props) => { + const { t, ...restProps } = props const ChartDatasetSelectorOptions = React.useMemo( () => [ - { value: "grafic_extensio", label: t("Extensió") }, - { value: "grafic_risc_iepg", label: t("Risc iEPG") }, - { value: "seguiment", label: t("Tracking") }, - { value: "situacio", label: t("Situation") }, + { value: 'grafic_extensio', label: t('Extensió') }, + { value: 'grafic_risc_iepg', label: t('Risc iEPG') }, + { value: 'seguiment', label: t('Tracking') }, + { value: 'situacio', label: t('Situation') } ], [t] - ); + ) return ( <Selector - label={t("Dataset")} - help={t("Select the dataset")} + label={t('Dataset')} + help={t('Select the dataset')} options={ChartDatasetSelectorOptions} {...restProps} /> - ); -}); + ) +}) /* Renders the edit form for the Map widget */ class Edit extends React.PureComponent { onChangeChartDivision = (event) => - this.props.onChangeChartDivision(event.target.value); + this.props.onChangeChartDivision(event.target.value) + onChangeChartPopulation = (event) => - this.props.onChangeChartPopulation(event.target.value); + this.props.onChangeChartPopulation(event.target.value) + onChangeChartDataset = (event) => - this.props.onChangeChartDataset(event.target.value); - onChangeChartRegion = (value) => this.props.onChangeChartRegion(value); + this.props.onChangeChartDataset(event.target.value) - render() { + onChangeChartRegion = (value) => this.props.onChangeChartRegion(value) + + render () { const { chartDivision, chartPopulation, @@ -154,32 +159,32 @@ class Edit extends React.PureComponent { divisions, populations, chartsIndex, - t, - } = this.props; + t + } = this.props return ( - <div style={{ textAlign: "left" }}> + <div style={{ textAlign: 'left' }}> <ChartDivisionSelector divisions={divisions} value={chartDivision} onChange={this.onChangeChartDivision} /> - <Divider style={{ margin: "2em 0" }} /> + <Divider style={{ margin: '2em 0' }} /> <ChartPopulationSelector populations={populations} value={chartPopulation} onChange={this.onChangeChartPopulation} /> - <Divider style={{ margin: "2em 0" }} /> + <Divider style={{ margin: '2em 0' }} /> <ChartDatasetSelector value={chartDataset} onChange={this.onChangeChartDataset} /> - <Divider style={{ margin: "2em 0" }} /> - <FormDecorators id={"regions-tree"} label={t("Region")} fixTree> + <Divider style={{ margin: '2em 0' }} /> + <FormDecorators id='regions-tree' label={t('Region')} fixTree> <ChartRegionSelector - id={"regions-tree"} + id='regions-tree' chartsIndex={chartsIndex} division={chartDivision} population={chartPopulation} @@ -188,7 +193,7 @@ class Edit extends React.PureComponent { /> </FormDecorators> </div> - ); + ) } } @@ -201,7 +206,7 @@ Edit.propTypes = { onChangeChartDivision: PropTypes.func.isRequired, onChangeChartPopulation: PropTypes.func.isRequired, onChangeChartDataset: PropTypes.func.isRequired, - onChangeChartRegion: PropTypes.func.isRequired, -}; + onChangeChartRegion: PropTypes.func.isRequired +} -export default translate("Widget/Chart/Edit")(Edit); +export default translate('Widget/Chart/Edit')(Edit) diff --git a/app/src/Widget/Widgets/Chart/EditRegion/EditRegion.jsx b/app/src/Widget/Widgets/Chart/EditRegion/EditRegion.jsx index 805cca28..a0db6a7e 100644 --- a/app/src/Widget/Widgets/Chart/EditRegion/EditRegion.jsx +++ b/app/src/Widget/Widgets/Chart/EditRegion/EditRegion.jsx @@ -1,11 +1,11 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { withHandler } from "../../../../Backend/Charts/context"; +import { withHandler } from '../../../../Backend/Charts/context' -import RecursiveTreeView from "./RecursiveTreeView"; +import RecursiveTreeView from './RecursiveTreeView' -function EditRegion(props) { +function EditRegion (props) { const { // Remove it from restProps: it comes from // Charts handler, not parent component @@ -17,20 +17,20 @@ function EditRegion(props) { onChange, chartsDataHandler, ...restProps - } = props; + } = props - const onNodeSelect = (event, v) => onChange(Number(v)); + const onNodeSelect = (event, v) => onChange(Number(v)) const initialNode = React.useMemo( () => chartsDataHandler.findInitialNode(division, population), [chartsDataHandler, division, population] - ); + ) const found = React.useMemo( () => chartsDataHandler .findBreadcrumb(initialNode, value) .map(({ url }) => `${url}`), [initialNode, value, chartsDataHandler] - ); + ) return ( <RecursiveTreeView @@ -40,7 +40,7 @@ function EditRegion(props) { value={`${value}`} {...restProps} /> - ); + ) } const EditRegionPropTypes = { @@ -50,14 +50,14 @@ const EditRegionPropTypes = { onChange: PropTypes.func.isRequired, chartsDataHandler: PropTypes.shape({ findInitialNode: PropTypes.func.isRequired, - findBreadcrumb: PropTypes.func.isRequired, + findBreadcrumb: PropTypes.func.isRequired }).isRequired, - chartsIndex: PropTypes.string, -}; + chartsIndex: PropTypes.string +} -EditRegion.propTypes = EditRegionPropTypes; +EditRegion.propTypes = EditRegionPropTypes EditRegion.defaultProps = { - chartsIndex: "", -}; + chartsIndex: '' +} -export default withHandler(EditRegion); +export default withHandler(EditRegion) diff --git a/app/src/Widget/Widgets/Chart/EditRegion/RecursiveTreeView.jsx b/app/src/Widget/Widgets/Chart/EditRegion/RecursiveTreeView.jsx index 3fb2ec7f..44ff5663 100644 --- a/app/src/Widget/Widgets/Chart/EditRegion/RecursiveTreeView.jsx +++ b/app/src/Widget/Widgets/Chart/EditRegion/RecursiveTreeView.jsx @@ -1,23 +1,23 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import TreeView from "@material-ui/lab/TreeView"; -import { makeStyles } from "@material-ui/core/styles"; -import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; -import ChevronRightIcon from "@material-ui/icons/ChevronRight"; +import TreeView from '@material-ui/lab/TreeView' +import { makeStyles } from '@material-ui/core/styles' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' -import RenderTree, { RenderTreePropTypesShape } from "./RenderTree"; +import RenderTree, { RenderTreePropTypesShape } from './RenderTree' const useStyles = makeStyles({ root: { height: 110, flexGrow: 1, - maxWidth: 400, - }, -}); + maxWidth: 400 + } +}) -function RecursiveTreeView({ value, found, node, onNodeSelect, ...props }) { - const classes = useStyles(); +function RecursiveTreeView ({ value, found, node, onNodeSelect, ...props }) { + const classes = useStyles() return ( <TreeView @@ -33,17 +33,17 @@ function RecursiveTreeView({ value, found, node, onNodeSelect, ...props }) { {node.children} </RenderTree> </TreeView> - ); + ) } const RecursiveTreeViewPropTypes = { value: PropTypes.string.isRequired, found: PropTypes.arrayOf(PropTypes.string).isRequired, node: RenderTreePropTypesShape.isRequired, - onNodeSelect: PropTypes.func.isRequired, -}; + onNodeSelect: PropTypes.func.isRequired +} -RecursiveTreeView.propTypes = RecursiveTreeViewPropTypes; +RecursiveTreeView.propTypes = RecursiveTreeViewPropTypes -export default RecursiveTreeView; -export { RecursiveTreeViewPropTypes }; +export default RecursiveTreeView +export { RecursiveTreeViewPropTypes } diff --git a/app/src/Widget/Widgets/Chart/EditRegion/RenderTree.jsx b/app/src/Widget/Widgets/Chart/EditRegion/RenderTree.jsx index 543278ea..1de30e6b 100644 --- a/app/src/Widget/Widgets/Chart/EditRegion/RenderTree.jsx +++ b/app/src/Widget/Widgets/Chart/EditRegion/RenderTree.jsx @@ -1,11 +1,11 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import TreeItem from "@material-ui/lab/TreeItem"; +import TreeItem from '@material-ui/lab/TreeItem' // From: https://material-ui.com/components/tree-view/#rich-object -function RenderTree({ url, name, children = [] }) { +function RenderTree ({ url, name, children = [] }) { return ( <TreeItem nodeId={`${url}`} label={name}> {children.map((child) => ( @@ -14,29 +14,29 @@ function RenderTree({ url, name, children = [] }) { </RenderTree> ))} </TreeItem> - ); + ) } // Recursive PropTypes const RenderTreePropTypes = { name: PropTypes.string.isRequired, - url: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, -}; + url: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired +} -const RenderTreePropTypesShape = PropTypes.shape(RenderTreePropTypes); +const RenderTreePropTypesShape = PropTypes.shape(RenderTreePropTypes) RenderTreePropTypes.children = PropTypes.oneOfType([ PropTypes.arrayOf(RenderTreePropTypesShape), - PropTypes.any, -]); + PropTypes.any +]) -RenderTree.propTypes = RenderTreePropTypes; +RenderTree.propTypes = RenderTreePropTypes RenderTree.defaultProps = { - children: [], -}; + children: [] +} // Cache the tree -const RenderTreeMemoized = React.memo(RenderTree); +const RenderTreeMemoized = React.memo(RenderTree) -export default RenderTreeMemoized; -export { RenderTreePropTypes, RenderTreePropTypesShape }; +export default RenderTreeMemoized +export { RenderTreePropTypes, RenderTreePropTypesShape } diff --git a/app/src/Widget/Widgets/Chart/EditRegion/index.js b/app/src/Widget/Widgets/Chart/EditRegion/index.js index 2c4c65c6..4c5ae330 100644 --- a/app/src/Widget/Widgets/Chart/EditRegion/index.js +++ b/app/src/Widget/Widgets/Chart/EditRegion/index.js @@ -1,3 +1,3 @@ -import EditRegion from "./EditRegion"; +import EditRegion from './EditRegion' -export default EditRegion; +export default EditRegion diff --git a/app/src/Widget/Widgets/Chart/Widget.jsx b/app/src/Widget/Widgets/Chart/Widget.jsx index 8d32b409..78de80a3 100644 --- a/app/src/Widget/Widgets/Chart/Widget.jsx +++ b/app/src/Widget/Widgets/Chart/Widget.jsx @@ -1,23 +1,23 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faEdit, - faChartArea as faChart, -} from "@fortawesome/free-solid-svg-icons"; + faChartArea as faChart +} from '@fortawesome/free-solid-svg-icons' -import { withHandler, withData } from "../../../Backend/Charts/context"; +import { withHandler, withData } from '../../../Backend/Charts/context' -import Chart from "./MultiChart"; -import Edit from "./Edit"; -import withWidget from "../../Widget"; +import Chart from './MultiChart' +import Edit from './Edit' +import withWidget from '../../Widget' const ChartWrapper = withWidget({ // The normal view view: { icon: <FontAwesomeIcon icon={faChart} />, - label: ({ t }) => t("View"), + label: ({ t }) => t('View'), title: ({ title }) => title, // withData: Uses {chartPopulation, chartDivision, chartRegion} props to handle `chartDataset` download @@ -33,12 +33,12 @@ const ChartWrapper = withWidget({ // Used setChartData, indexValues, - id, - } = props; + id + } = props // Once downloaded, set parent's data, so it can properly // set title and other widget components - React.useEffect(() => setChartData(valors), [valors, setChartData]); + React.useEffect(() => setChartData(valors), [valors, setChartData]) return ( <Chart @@ -49,36 +49,36 @@ const ChartWrapper = withWidget({ valors, population, region, - dataset, + dataset }} /> - ); - }), + ) + }) }, // Edit data edit: { icon: <FontAwesomeIcon icon={faEdit} />, - label: ({ t }) => t("Edit"), - title: ({ t }) => t("Edit chart parameters"), - render: Edit, - }, -}); + label: ({ t }) => t('Edit'), + title: ({ t }) => t('Edit chart parameters'), + render: Edit + } +}) /* Combine ChartData backend with Chart */ class DataHandler extends React.Component { state = { - chartData: false, - }; + chartData: false + } - constructor(props) { - super(props); + constructor (props) { + super(props) // Default values: first element of each's group - const { chartDivision, chartPopulation, chartRegion, chartDataset } = props; - const { chartData } = this.state; + const { chartDivision, chartPopulation, chartRegion, chartDataset } = props + const { chartData } = this.state // Used to find metadata from selected options // and for the rest of options while editing @@ -90,8 +90,8 @@ class DataHandler extends React.Component { chartRegion, chartDataset, chartData - ), - }; + ) + } } // Get chartData from child @@ -100,9 +100,9 @@ class DataHandler extends React.Component { // - Allows continue editing once a value has changed setChartData = (chartData) => { if (chartData !== this.state.chartData) { - this.setState({ chartData }); + this.setState({ chartData }) } - }; + } // Get metadata from given params getMeta = ( @@ -117,30 +117,30 @@ class DataHandler extends React.Component { chartDivision, chartPopulation, chartRegion - ); + ) if (!chartData) { return { - title: "...", - name: "...", - node, - }; + title: '...', + name: '...', + node + } } const title = - chartData[chartDataset]?.title || chartData[chartDataset]?.name; + chartData[chartDataset]?.title || chartData[chartDataset]?.name return { title, name: title, - node, - }; - }; + node + } + } // Set map meta data updateMetadata = () => { const { chartDivision, chartPopulation, chartRegion, chartDataset } = - this.props; - const { chartData } = this.state; + this.props + const { chartData } = this.state this.setState({ ...this.getMeta( @@ -149,14 +149,14 @@ class DataHandler extends React.Component { chartRegion, chartDataset, chartData - ), - }); - }; + ) + }) + } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate (prevProps, prevState) { const { chartDivision, chartPopulation, chartRegion, chartDataset } = - this.props; - const { chartData } = this.state; + this.props + const { chartData } = this.state // If chartData is unset, gather new data if ( @@ -167,7 +167,7 @@ class DataHandler extends React.Component { chartDataset !== prevProps.chartDataset || chartData !== prevState.chartData ) { - this.updateMetadata(); + this.updateMetadata() } } @@ -179,66 +179,66 @@ class DataHandler extends React.Component { chartRegion, chartDataset ) => { - this.updateMetadata(); - const { chartsDataHandler, onChangeData, id } = this.props; + this.updateMetadata() + const { chartsDataHandler, onChangeData, id } = this.props const region = chartsDataHandler.findRegion( chartDivision, chartPopulation, chartRegion - ); + ) onChangeData(id, { chartDivision, chartPopulation, chartDataset, - chartRegion: region.url, - }); - }; + chartRegion: region.url + }) + } // Force data update on division change onChangeChartDivision = (chartDivision) => { - const { chartPopulation, chartRegion, chartDataset } = this.props; + const { chartPopulation, chartRegion, chartDataset } = this.props this.onChangeChart( chartDivision, chartPopulation, chartRegion, chartDataset - ); - }; + ) + } // Force data update on value change onChangeChartPopulation = (chartPopulation) => { - const { chartDivision, chartRegion, chartDataset } = this.props; + const { chartDivision, chartRegion, chartDataset } = this.props this.onChangeChart( chartDivision, chartPopulation, chartRegion, chartDataset - ); - }; + ) + } // Force data update on value change onChangeChartRegion = (chartRegion) => { - const { chartDivision, chartPopulation, chartDataset } = this.props; + const { chartDivision, chartPopulation, chartDataset } = this.props this.onChangeChart( chartDivision, chartPopulation, chartRegion, chartDataset - ); - }; + ) + } // Datasets are on same data object onChangeChartDataset = (chartDataset) => { - const { chartDivision, chartPopulation, chartRegion } = this.props; + const { chartDivision, chartPopulation, chartRegion } = this.props this.onChangeChart( chartDivision, chartPopulation, chartRegion, chartDataset - ); - }; + ) + } - render() { + render () { const { id, days, @@ -248,17 +248,17 @@ class DataHandler extends React.Component { chartDataset, chartRegion, chartsDataHandler: { divisions, populations }, - onRemove, - } = this.props; - const { title, name, node } = this.state; + onRemove + } = this.props + const { title, name, node } = this.state return ( <div style={{ - minWidth: "200px", - height: "100%", - paddingTop: ".3em", - flex: "1 1 0px", + minWidth: '200px', + height: '100%', + paddingTop: '.3em', + flex: '1 1 0px' }} > <ChartWrapper @@ -273,7 +273,7 @@ class DataHandler extends React.Component { indexValues, days, title, - name, + name }} /* Used in Edit */ divisions={divisions} @@ -285,15 +285,15 @@ class DataHandler extends React.Component { onRemove={onRemove} /> </div> - ); + ) } } // Default values: first element of each's group -const chartDivision = "REGIÓ/AGA"; -const chartPopulation = "Població total"; -const chartRegion = 0; -const chartDataset = "grafic_extensio"; +const chartDivision = 'REGIÓ/AGA' +const chartPopulation = 'Població total' +const chartRegion = 0 +const chartDataset = 'grafic_extensio' DataHandler.defaultProps = { chartDivision, @@ -301,8 +301,8 @@ DataHandler.defaultProps = { chartRegion, chartDataset, onChangeData: () => {}, - onRemove: () => {}, -}; + onRemove: () => {} +} DataHandler.propTypes = { days: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -314,7 +314,7 @@ DataHandler.propTypes = { chartPopulation: PropTypes.string.isRequired, chartDataset: PropTypes.string.isRequired, chartRegion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - .isRequired, -}; + .isRequired +} -export default withHandler(DataHandler); +export default withHandler(DataHandler) diff --git a/app/src/Widget/Widgets/Common/Chart.jsx b/app/src/Widget/Widgets/Common/Chart.jsx index 0297282b..8e395c92 100644 --- a/app/src/Widget/Widgets/Common/Chart.jsx +++ b/app/src/Widget/Widgets/Common/Chart.jsx @@ -286,8 +286,8 @@ function Chart (props) { const yAxisLabel = yAxis?.label || '' const scale = yAxis?.scale ? yAxis.scale === 'squarified' - ? 'pow' - : yAxis.scale + ? 'pow' + : yAxis.scale : 'linear' const { ResponsiveContainer, ComposedChart, ReferenceLine, XAxis, YAxis } = diff --git a/app/src/Widget/Widgets/Common/ChartTooltip.jsx b/app/src/Widget/Widgets/Common/ChartTooltip.jsx index b0f72829..c09ee9fc 100644 --- a/app/src/Widget/Widgets/Common/ChartTooltip.jsx +++ b/app/src/Widget/Widgets/Common/ChartTooltip.jsx @@ -133,12 +133,14 @@ ChartLegend.propTypes = { const ChartTooltip = ({ active, payload, label }) => { const classes = useStyles() - return active ? ( - <div className={classes.tooltip}> - <p className={classes.label}>{label}</p> - <List payload={payload} /> - </div> - ) : null + return active + ? ( + <div className={classes.tooltip}> + <p className={classes.label}>{label}</p> + <List payload={payload} /> + </div> + ) + : null } ChartTooltip.defaultProps = { diff --git a/app/src/Widget/Widgets/Common/FormDecorators.jsx b/app/src/Widget/Widgets/Common/FormDecorators.jsx index 9074df0c..1d510e7e 100644 --- a/app/src/Widget/Widgets/Common/FormDecorators.jsx +++ b/app/src/Widget/Widgets/Common/FormDecorators.jsx @@ -40,9 +40,11 @@ const FormDecorators = (props) => { {label} </InputLabel> {children} - {help ? ( - <FormHelperText id={`helper-text-${id}`}>{help}</FormHelperText> - ) : null} + {help + ? ( + <FormHelperText id={`helper-text-${id}`}>{help}</FormHelperText> + ) + : null} </FormControl> </div> ) diff --git a/app/src/Widget/Widgets/Common/Selector.jsx b/app/src/Widget/Widgets/Common/Selector.jsx index a97f0b93..d79f464d 100644 --- a/app/src/Widget/Widgets/Common/Selector.jsx +++ b/app/src/Widget/Widgets/Common/Selector.jsx @@ -16,7 +16,7 @@ const Selector = (props) => { aria-describedby={`helper-text-${id}`} inputProps={{ name: label, - id: id + id }} > {options.map((option) => ( diff --git a/app/src/Widget/Widgets/Map/Edit.jsx b/app/src/Widget/Widgets/Map/Edit.jsx index 89613987..fa6bd2ed 100644 --- a/app/src/Widget/Widgets/Map/Edit.jsx +++ b/app/src/Widget/Widgets/Map/Edit.jsx @@ -1,36 +1,36 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { translate } from "react-translate"; +import { translate } from 'react-translate' -import { makeStyles } from "@material-ui/core/styles"; -import InputLabel from "@material-ui/core/InputLabel"; -import FormHelperText from "@material-ui/core/FormHelperText"; -import FormControl from "@material-ui/core/FormControl"; -import Select from "@material-ui/core/Select"; -import Divider from "@material-ui/core/Divider"; +import { makeStyles } from '@material-ui/core/styles' +import InputLabel from '@material-ui/core/InputLabel' +import FormHelperText from '@material-ui/core/FormHelperText' +import FormControl from '@material-ui/core/FormControl' +import Select from '@material-ui/core/Select' +import Divider from '@material-ui/core/Divider' -import MapData from "../../../Backend/Maps"; +import MapData from '../../../Backend/Maps' // UI-Material Styles const useStyles = makeStyles((theme) => ({ formControl: { - minWidth: 120, + minWidth: 120 }, selectEmpty: { - marginTop: theme.spacing(2), - }, -})); + marginTop: theme.spacing(2) + } +})) // Renders a Select, with options from props const Selector = (props) => { - const classes = useStyles(); - const { label, help, id, options, ...restProps } = props; + const classes = useStyles() + const { label, help, id, options, ...restProps } = props return ( <div style={{ - margin: "0 auto", - display: "block", + margin: '0 auto', + display: 'block' }} > <FormControl required className={classes.formControl}> @@ -41,7 +41,7 @@ const Selector = (props) => { aria-describedby={`helper-text-${id}`} inputProps={{ name: label, - id: id, + id }} > {options.map(({ value, label }) => ( @@ -50,72 +50,74 @@ const Selector = (props) => { </option> ))} </Select> - {help ? ( - <FormHelperText id={`helper-text-${id}`}>{help}</FormHelperText> - ) : null} + {help + ? ( + <FormHelperText id={`helper-text-${id}`}>{help}</FormHelperText> + ) + : null} </FormControl> </div> - ); -}; + ) +} // Renders a Select with map kinds options const MapSelectorOptions = MapData.kinds().map((kind) => ({ value: kind, - label: kind, -})); -const MapSelector = translate("Widget/Map/Edit")((props) => { - const { t, ...restProps } = props; + label: kind +})) +const MapSelector = translate('Widget/Map/Edit')((props) => { + const { t, ...restProps } = props return ( <Selector options={MapSelectorOptions} - label={t("Type")} - help={t("Select the regions type to show")} + label={t('Type')} + help={t('Select the regions type to show')} {...restProps} /> - ); -}); + ) +}) // Renders a Select with map values options const MapValueSelectorOptions = MapData.kinds().reduce((options, kind) => { options[kind] = MapData.values(kind).map((values) => ({ value: values, - label: MapData.metaName(values), - })); - return options; -}, {}); -const MapValueSelector = translate("Widget/Map/Edit")((props) => { - const { kind, t, ...restProps } = props; + label: MapData.metaName(values) + })) + return options +}, {}) +const MapValueSelector = translate('Widget/Map/Edit')((props) => { + const { kind, t, ...restProps } = props return ( <Selector - label={t("Values")} - help={t("Select the data origin")} + label={t('Values')} + help={t('Select the data origin')} options={MapValueSelectorOptions[kind]} {...restProps} /> - ); -}); + ) +}) /* Renders the edit form for the Map widget */ class Edit extends React.PureComponent { - onChangeMapKind = (event) => this.props.onChangeMapKind(event.target.value); - onChangeMapValue = (event) => this.props.onChangeMapValue(event.target.value); + onChangeMapKind = (event) => this.props.onChangeMapKind(event.target.value) + onChangeMapValue = (event) => this.props.onChangeMapValue(event.target.value) - render() { - const { mapKind, mapValue } = this.props; + render () { + const { mapKind, mapValue } = this.props return ( - <div style={{ textAlign: "left" }}> + <div style={{ textAlign: 'left' }}> <MapSelector value={mapKind} onChange={this.onChangeMapKind} /> - <Divider style={{ margin: "2em 0" }} /> + <Divider style={{ margin: '2em 0' }} /> <MapValueSelector kind={mapKind} value={mapValue} onChange={this.onChangeMapValue} /> </div> - ); + ) } } @@ -123,7 +125,7 @@ Edit.propTypes = { mapKind: PropTypes.string.isRequired, mapValue: PropTypes.string.isRequired, onChangeMapKind: PropTypes.func.isRequired, - onChangeMapValue: PropTypes.func.isRequired, -}; + onChangeMapValue: PropTypes.func.isRequired +} -export default Edit; +export default Edit diff --git a/app/src/Widget/Widgets/Map/Legend.jsx b/app/src/Widget/Widgets/Map/Legend.jsx index bed9b7dd..48c80caf 100644 --- a/app/src/Widget/Widgets/Map/Legend.jsx +++ b/app/src/Widget/Widgets/Map/Legend.jsx @@ -38,13 +38,15 @@ Element.propTypes = { // Renders children conditionally wrapped with a tooltip, // depending on the existence and length of `title` prop const ConditionalTooltip = ({ title, children, ...rest }) => - title && title.length ? ( - <Tooltip arrow placement='top' title={title} {...rest}> - {children} - </Tooltip> - ) : ( - children - ) + title && title.length + ? ( + <Tooltip arrow placement='top' title={title} {...rest}> + {children} + </Tooltip> + ) + : ( + children + ) ConditionalTooltip.defaultProps = { children: [] diff --git a/app/src/Widget/Widgets/Map/Map.css b/app/src/Widget/Widgets/Map/Map.css index ebfd13d9..855e0a32 100644 --- a/app/src/Widget/Widgets/Map/Map.css +++ b/app/src/Widget/Widgets/Map/Map.css @@ -5,7 +5,10 @@ stroke-linejoin: round; transform-box: fill-box; transform-origin: center; - transition: transform 0.1s, stroke-width 0.2s, stroke 0.1s; + transition: + transform 0.1s, + stroke-width 0.2s, + stroke 0.1s; paint-order: stroke; } diff --git a/app/src/Widget/Widgets/Map/MapImage.jsx b/app/src/Widget/Widgets/Map/MapImage.jsx index 4bcd39ce..f9fdd087 100644 --- a/app/src/Widget/Widgets/Map/MapImage.jsx +++ b/app/src/Widget/Widgets/Map/MapImage.jsx @@ -1,51 +1,51 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { styled } from "@material-ui/core/styles"; +import { styled } from '@material-ui/core/styles' -import { ReactSVG } from "react-svg"; -import ReactTooltip from "react-tooltip"; +import { ReactSVG } from 'react-svg' +import ReactTooltip from 'react-tooltip' -import Loading from "../../../Loading"; -import LegendElement from "../Common/LegendElement"; +import Loading from '../../../Loading' +import LegendElement from '../Common/LegendElement' -import "./Map.css"; +import './Map.css' // Renders ReactTooltip with MaterialUI theme styles const ReactTooltipStyled = styled(ReactTooltip)(({ theme }) => ({ backgroundColor: `${theme.palette.background.default} !important`, color: `${theme.palette.text.primary} !important`, - opacity: "1 !important", + opacity: '1 !important', borderColor: `${theme.palette.text.hint} !important`, ...theme.shape, fontFamily: theme.typography.fontFamily, fontSize: theme.typography.fontSize, - "&:before": { - borderTopColor: `${theme.palette.text.hint} !important`, + '&:before': { + borderTopColor: `${theme.palette.text.hint} !important` }, - "&:after": { - borderTopColor: `${theme.palette.background.default} !important`, - }, -})); + '&:after': { + borderTopColor: `${theme.palette.background.default} !important` + } +})) // Helper functions for ReactTooltip -const fallback = () => <span>Error!</span>; /* Should never happen */ -const loading = () => <Loading />; +const fallback = () => <span>Error!</span> /* Should never happen */ +const loading = () => <Loading /> const TipContent = React.memo( ({ id, valuesDay, label, element, colorGetter }) => { - const valueRaw = valuesDay.get(id); - const color = colorGetter(valueRaw); - const value = `${label}: ${valueRaw ?? ""}`; - const title = element.getAttribute("label"); + const valueRaw = valuesDay.get(id) + const color = colorGetter(valueRaw) + const value = `${label}: ${valueRaw ?? ''}` + const title = element.getAttribute('label') return ( <> <h4>{title}</h4> - <LegendElement {...{ value, color }} justify="center" /> + <LegendElement {...{ value, color }} justify='center' /> </> - ); + ) } -); +) /* Renders an SVG map, allowing to change each region background color and tooltip @@ -57,116 +57,116 @@ const TipContent = React.memo( Some of the code here has been inspired in the original from https://dadescovid.org */ class MapImage extends React.Component { - svg = null; + svg = null state = { // Used to force update based on side effects - svgStatus: null, - }; + svgStatus: null + } // Save SVG DOM node reference // This way we can access it's regions to modify them - wrapperNodeSVG = React.createRef(); + wrapperNodeSVG = React.createRef() // Get the SVG node itself isSVGMounted = () => { return ( - this?.wrapperNodeSVG?.current?.container?.querySelector(`svg`) !== null && + this?.wrapperNodeSVG?.current?.container?.querySelector('svg') !== null && this.svg !== null - ); - }; + ) + } // Gets the color for a given value (in func params) in a colors table (in props) getColor = (value) => { - const color = this.props.colors.find((c) => value >= c.value); - return color ? color.color : "fff"; - }; + const color = this.props.colors.find((c) => value >= c.value) + return color ? color.color : 'fff' + } // Sets the background color in a SVG region for a given value setColorBackground = (element, value) => { - element.style.fill = this.getColor(value); - }; + element.style.fill = this.getColor(value) + } // Sets the background color for all regions in values prop fillColors = (props) => { if (this.timer) { - return; + return } this.timer = requestAnimationFrame(() => { - const values = props.values[props.indexValues] || {}; - const { mapElements } = this; + const values = props.values[props.indexValues] || {} + const { mapElements } = this for (const [id, value] of values.entries()) { - const element = mapElements.get(id); + const element = mapElements.get(id) if (element !== undefined) { - this.setColorBackground(element, value); + this.setColorBackground(element, value) } } - this.timer = false; - }, 0); - }; + this.timer = false + }, 0) + } // Sets data into a map region to be used by the tooltip renderer) setTooltipData = (element) => { - const dataFor = `mapa-${this.props.id}`; - element.setAttribute("data-tip", element.id); - element.setAttribute("data-for", dataFor); - }; + const dataFor = `mapa-${this.props.id}` + element.setAttribute('data-tip', element.id) + element.setAttribute('data-for', dataFor) + } // Save an Map of the map elements, indexed by their `id` saveElementsIndex = (svg) => { this.mapElements = new Map( Array.prototype.map.call( - this.svg.querySelectorAll(`.map_elem`), + this.svg.querySelectorAll('.map_elem'), (current) => { - this.setTooltipData(current); - return [current.id, current]; + this.setTooltipData(current) + return [current.id, current] } ) - ); - }; + ) + } // Process data into the SVG DOM node before first injecting it into the DOM tree beforeInjection = (svg) => { - this.svg = svg; - this.saveElementsIndex(); - this.setState({ svgStatus: "beforeInjection" }); - }; + this.svg = svg + this.saveElementsIndex() + this.setState({ svgStatus: 'beforeInjection' }) + } // Get SVG DOM node and reset colors from data if needed afterInjection = (error, svg) => { if (error) { - throw Error(error); + throw Error(error) } // Call ReactTooltip to rebuild its database with new objects - ReactTooltip.rebuild(); - this.svg = svg; - this.fillColors(this.props); - this.setState({ svgStatus: "afterInjection" }); - }; + ReactTooltip.rebuild() + this.svg = svg + this.fillColors(this.props) + this.setState({ svgStatus: 'afterInjection' }) + } // Update background colors if SVG is mounted updateColorsIfPossible = (props) => { if (this.isSVGMounted()) { - this.fillColors(props); + this.fillColors(props) } - }; + } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate (prevProps, prevState) { if (this.props.mapSrc !== prevProps.mapSrc) { - this.setState({ svgStatus: "srcChanged" }); + this.setState({ svgStatus: 'srcChanged' }) } } - componentDidMount() { - this.updateColorsIfPossible(this.props); - this.setState({ svgStatus: "mounted" }); + componentDidMount () { + this.updateColorsIfPossible(this.props) + this.setState({ svgStatus: 'mounted' }) } // Help GC about side effects - componentWillUnmount() { + componentWillUnmount () { if (this.timer) { - cancelAnimationFrame(this.timer); + cancelAnimationFrame(this.timer) } } @@ -174,39 +174,39 @@ class MapImage extends React.Component { // - Only render on map/svg changes // - Trigger color update on indexUpdate, but // avoid render if this is the only change - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate (nextProps, nextState) { if (this.props.indexValues !== nextProps.indexValues) { - this.updateColorsIfPossible(nextProps); + this.updateColorsIfPossible(nextProps) if (this.state.tooltipShow) { // It's slow, but it's the faster solution found to update tooltip content // without re-drawing full tooltip component (very slow) - ReactTooltip.show(this.mapElements.get(this.state.tooltipShow)); + ReactTooltip.show(this.mapElements.get(this.state.tooltipShow)) } } if ( this.props.mapSrc !== nextProps.mapSrc || this.state.svgStatus !== nextState.svgStatus ) { - return true; + return true } - return false; + return false } // Renders the tip contents for a region - afterTipShow = (event) => this.setState({ tooltipShow: event.target.id }); - afterTipHide = (event) => this.setState({ tooltipShow: false }); + afterTipShow = (event) => this.setState({ tooltipShow: event.target.id }) + afterTipHide = (event) => this.setState({ tooltipShow: false }) getTipContent = (id) => { if (!id) { - return ""; + return '' } - const element = this.mapElements?.get(id) ?? false; + const element = this.mapElements?.get(id) ?? false if (!element) { - return ""; + return '' } - const { values, indexValues, label } = this.props; - const valuesDay = values[indexValues]; + const { values, indexValues, label } = this.props + const valuesDay = values[indexValues] return ( <TipContent {...{ @@ -214,24 +214,24 @@ class MapImage extends React.Component { valuesDay, label, element, - colorGetter: this.getColor, + colorGetter: this.getColor }} /> - ); - }; + ) + } - render() { - const { mapSrc, title, id } = this.props; + render () { + const { mapSrc, title, id } = this.props return ( <> <ReactSVG src={mapSrc} ref={this.wrapperNodeSVG} - role="img" + role='img' aria-label={title} beforeInjection={this.beforeInjection} afterInjection={this.afterInjection} - evalScripts="never" + evalScripts='never' fallback={fallback} loading={loading} renumerateIRIElements={false} @@ -241,19 +241,19 @@ class MapImage extends React.Component { getContent={this.getTipContent} afterShow={this.afterTipShow} afterHide={this.afterTipHide} - border={true} + border /> </> - ); + ) } } MapImage.defaultProps = { values: [], indexValues: 0, - label: "Index", - title: "Map", -}; + label: 'Index', + title: 'Map' +} MapImage.propTypes = { label: PropTypes.string, @@ -264,10 +264,10 @@ MapImage.propTypes = { colors: PropTypes.arrayOf( PropTypes.shape({ value: PropTypes.number.isRequired, - color: PropTypes.string.isRequired, + color: PropTypes.string.isRequired }) ).isRequired, - indexValues: PropTypes.number.isRequired, -}; + indexValues: PropTypes.number.isRequired +} -export default MapImage; +export default MapImage diff --git a/app/src/Widget/Widgets/Map/Widget.jsx b/app/src/Widget/Widgets/Map/Widget.jsx index f9e74153..3798d285 100644 --- a/app/src/Widget/Widgets/Map/Widget.jsx +++ b/app/src/Widget/Widgets/Map/Widget.jsx @@ -1,31 +1,31 @@ -import React from "react"; -import PropTypes from "prop-types"; +import React from 'react' +import PropTypes from 'prop-types' -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faInfoCircle as faLegend, faEdit, - faGlobeEurope as faMap, -} from "@fortawesome/free-solid-svg-icons"; + faGlobeEurope as faMap +} from '@fortawesome/free-solid-svg-icons' -import MapData from "../../../Backend/Maps"; -import { withData } from "../../../Backend/Maps/context"; +import MapData from '../../../Backend/Maps' +import { withData } from '../../../Backend/Maps/context' -import MapImage from "./MapImage"; -import Edit from "./Edit"; -import Legend from "./Legend"; -import withWidget from "../../Widget"; +import MapImage from './MapImage' +import Edit from './Edit' +import Legend from './Legend' +import withWidget from '../../Widget' const MapWrapper = withWidget({ // The normal view view: { icon: <FontAwesomeIcon icon={faMap} />, - label: ({ t }) => t("View"), + label: ({ t }) => t('View'), title: (props) => props.name, render: withData( ({ t, mapKind, label, mapData, indexValues, colors, id }) => ( <MapImage - title={`${t("Map")}: Catalunya: ${t(mapKind)}`} + title={`${t('Map')}: Catalunya: ${t(mapKind)}`} label={label} values={mapData.valors} mapSrc={MapData.svg(mapKind)} @@ -34,13 +34,13 @@ const MapWrapper = withWidget({ id={id} /> ) - ), + ) }, // Edit data edit: { icon: <FontAwesomeIcon icon={faEdit} />, - label: ({ t }) => t("Edit"), - title: ({ t }) => t("Edit map parameters"), + label: ({ t }) => t('Edit'), + title: ({ t }) => t('Edit map parameters'), render: (props) => ( <Edit mapKind={props.mapKind} @@ -48,36 +48,36 @@ const MapWrapper = withWidget({ onChangeMapKind={props.onChangeMapKind} onChangeMapValue={props.onChangeMapValue} /> - ), + ) }, // Show map legend legend: { icon: <FontAwesomeIcon icon={faLegend} />, - label: ({ t }) => t("Legend"), + label: ({ t }) => t('Legend'), title: (props) => props.title, render: (props) => ( <Legend colors={props.colors} subtitle={props.days[props.indexValues]} /> - ), - }, -}); + ) + } +}) /* Combine MapData backend with MapWrapper/MapImage */ class DataHandler extends React.Component { - constructor(props) { - super(props); + constructor (props) { + super(props) // Default values: first element of each's group // TODO: Get data from storage/router/props - const { mapKind, mapValue } = props; + const { mapKind, mapValue } = props this.state = { mapKindDefault: mapKind, mapValueDefault: mapValue, - ...this.getMeta(mapValue), - }; + ...this.getMeta(mapValue) + } } // Get metadata from given params @@ -85,42 +85,42 @@ class DataHandler extends React.Component { colors: MapData.metaColors(mapValue), title: MapData.metaTitle(mapValue), label: MapData.metaLabel(mapValue), - name: MapData.metaLabel(mapValue), - }); + name: MapData.metaLabel(mapValue) + }) // Update map metadata onChangeMapKind = (mapKind) => { - const { mapValue } = this.props; + const { mapValue } = this.props this.props.onChangeData(this.props.id, { mapKind, - mapValue, - }); - }; + mapValue + }) + } // Update map metadata onChangeMapValue = (mapValue) => { - const { mapKind } = this.props; + const { mapKind } = this.props this.setState({ - ...this.getMeta(mapValue), - }); + ...this.getMeta(mapValue) + }) this.props.onChangeData(this.props.id, { mapKind, - mapValue, - }); - }; + mapValue + }) + } - render() { - const { days, indexValues, id, mapKind, mapValue } = this.props; + render () { + const { days, indexValues, id, mapKind, mapValue } = this.props const { mapKindDefault, mapValueDefault, colors, title, label, name } = - this.state; + this.state return ( <div style={{ - minWidth: "200px", - height: "100%", - paddingTop: ".3em", - flex: "1 1 0px", + minWidth: '200px', + height: '100%', + paddingTop: '.3em', + flex: '1 1 0px' }} > <MapWrapper @@ -140,20 +140,20 @@ class DataHandler extends React.Component { name={name} /> </div> - ); + ) } } // Default values: first element of each's group -const mapKind = MapData.kinds()[0]; -const mapValue = MapData.values(mapKind)[0]; +const mapKind = MapData.kinds()[0] +const mapValue = MapData.values(mapKind)[0] DataHandler.defaultProps = { mapKind, mapValue, onChangeData: () => {}, - onRemove: () => {}, -}; + onRemove: () => {} +} DataHandler.propTypes = { days: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -162,7 +162,7 @@ DataHandler.propTypes = { onChangeData: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired, mapKind: PropTypes.string.isRequired, - mapValue: PropTypes.string.isRequired, -}; + mapValue: PropTypes.string.isRequired +} -export default DataHandler; +export default DataHandler diff --git a/app/src/asyncComponent.jsx b/app/src/asyncComponent.jsx index 0ca184e2..ead33d21 100644 --- a/app/src/asyncComponent.jsx +++ b/app/src/asyncComponent.jsx @@ -55,11 +55,13 @@ const asyncModuleComponent = (importModule, modules, Wrapped) => }, []) // Once loaded, pass components to wrapped as a prop - return !components ? ( - <Loading /> - ) : ( - <Wrapped {...props} {...{ components }} /> - ) + return !components + ? ( + <Loading /> + ) + : ( + <Wrapped {...props} {...{ components }} /> + ) } export default asyncComponent diff --git a/app/src/i18n/available.test.js b/app/src/i18n/available.test.js index c143916b..d541c63f 100644 --- a/app/src/i18n/available.test.js +++ b/app/src/i18n/available.test.js @@ -1,54 +1,54 @@ -import available from "./available"; +import available from './available' -test("languages are defined correctly (minimally)", () => { +test('languages are defined correctly (minimally)', () => { const groups = { - en: ["en", "en-en", "en-us", "en-au"], - fr: ["ca", "ca-es"], - es: ["es", "es-es"], - pl: ["pl", "pl-pl"], - no: ["nb-no"], - }; + en: ['en', 'en-en', 'en-us', 'en-au'], + fr: ['ca', 'ca-es'], + es: ['es', 'es-es'], + pl: ['pl', 'pl-pl'], + no: ['nb-no'] + } for (const locale of Object.keys(groups)) { for (const keyIndex of groups[locale]) { expect(available.find(({ key }) => key === keyIndex).value.locale).toBe( locale - ); + ) } } -}); +}) -test("all languages define all and only all translations defined in English", () => { - const english = available.find(({ key }) => key === "en").value; +test('all languages define all and only all translations defined in English', () => { + const english = available.find(({ key }) => key === 'en').value const languages = available - .filter(({ key }) => ["ca", "es", "pl", "nb-no"].includes(key)) - .map(({ value }) => value); + .filter(({ key }) => ['ca', 'es', 'pl', 'nb-no'].includes(key)) + .map(({ value }) => value) // All defined keys in English are defined in all languages const components = Object.keys(english).filter( - (key) => !["locale", "name"].includes(key) - ); + (key) => !['locale', 'name'].includes(key) + ) for (const component of components) { for (const text of Object.keys(english[component])) { // English key is equal to english translation try { - expect(english[component][text]).toBe(text); + expect(english[component][text]).toBe(text) } catch (err) { throw new Error( `Component '${component}' defined in English is NOT defined in English` - ); + ) } // All English keys are defined in all languages for (const language of languages) { try { - expect(language[component][text]).toBeDefined(); - expect(language[component][text].length).not.toBe(0); + expect(language[component][text]).toBeDefined() + expect(language[component][text].length).not.toBe(0) } catch (err) { throw new Error( `Text key '${text}' defined in component '${component}' defined in English is NOT defined in language '${language.locale}'` - ); + ) } } } @@ -57,29 +57,29 @@ test("all languages define all and only all translations defined in English", () // All defined keys and components in all languages are defined in English for (const language of languages) { const components = Object.keys(language).filter( - (key) => !["locale", "name"].includes(key) - ); + (key) => !['locale', 'name'].includes(key) + ) for (const component of components) { // Component defined in language is also defined in English try { - expect(english[component]).toBeDefined(); + expect(english[component]).toBeDefined() } catch (err) { throw new Error( `Component '${component}' defined in language '${language.locale}' is NOT defined in English` - ); + ) } // All keys defined in language's components are also defined in English' components for (const text of Object.keys(language[component])) { try { - expect(english[component][text]).toBeDefined(); + expect(english[component][text]).toBeDefined() } catch (err) { throw new Error( `Text key '${text}' defined in component '${component}' defined in language '${language.locale}' is NOT defined in English` - ); + ) } } } } -}); +}) diff --git a/app/src/testHelpers.js b/app/src/testHelpers.js index 3b63639f..8d5af0f5 100644 --- a/app/src/testHelpers.js +++ b/app/src/testHelpers.js @@ -1,102 +1,102 @@ -import React from "react"; -import { useLocation } from "react-router-dom"; +import React from 'react' +import { useLocation } from 'react-router-dom' -const delay = (millis) => new Promise((resolve) => setTimeout(resolve, millis)); +const delay = (millis) => new Promise((resolve) => setTimeout(resolve, millis)) -const catchConsoleWarn = async (cb, severity = "warn") => { - const output = []; - const originalWarn = console[severity]; +const catchConsoleWarn = async (cb, severity = 'warn') => { + const output = [] + const originalWarn = console[severity] const mockedWarn = jest.fn((msg, ...rest) => { - output.push(msg); + output.push(msg) if (rest) { - output.push(`${rest}`); + output.push(`${rest}`) } - }); - console[severity] = mockedWarn; - const value = await cb(); - console[severity] = originalWarn; + }) + console[severity] = mockedWarn + const value = await cb() + console[severity] = originalWarn return { value, output, - fn: mockedWarn, - }; -}; + fn: mockedWarn + } +} -const catchConsoleError = async (cb) => await catchConsoleWarn(cb, "error"); -const catchConsoleLog = async (cb) => await catchConsoleWarn(cb, "log"); -const catchConsoleDir = async (cb) => await catchConsoleWarn(cb, "dir"); +const catchConsoleError = async (cb) => await catchConsoleWarn(cb, 'error') +const catchConsoleLog = async (cb) => await catchConsoleWarn(cb, 'log') +const catchConsoleDir = async (cb) => await catchConsoleWarn(cb, 'dir') const createClientXY = (x, y) => ({ clientX: x, clientY: y, pageX: x, - pageY: y, -}); + pageY: y +}) const createStartTouchEventObject = ({ x = 0, y = 0 }) => ({ - touches: [createClientXY(x, y)], -}); + touches: [createClientXY(x, y)] +}) const createMoveTouchEventObject = (coords) => ({ touches: coords.map(({ x = 0, y = 0 }) => createClientXY(x, y)), - changedTouches: coords.map(({ x = 0, y = 0 }) => createClientXY(x, y)), -}); + changedTouches: coords.map(({ x = 0, y = 0 }) => createClientXY(x, y)) +}) const createEndTouchEventObject = ({ x = 0, y = 0 }) => ({ touches: [createClientXY(x, y)], - changedTouches: [createClientXY(x, y)], -}); + changedTouches: [createClientXY(x, y)] +}) // Testing components -const LocationDisplay = ({ member = "hash" }) => { - const location = useLocation(); - return <div data-testid="location-display">{location[member]}</div>; -}; +const LocationDisplay = ({ member = 'hash' }) => { + const location = useLocation() + return <div data-testid='location-display'>{location[member]}</div> +} // Helper to mock and test fetch class MockFetch { - original = null; + original = null options = { date: new Date(), throwError: false, responseOptions: () => ({ headers: { - "Content-Type": "application/json; charset=utf-8", - "last-modified": this.options.date.toString(), - }, - }), - }; + 'Content-Type': 'application/json; charset=utf-8', + 'last-modified': this.options.date.toString() + } + }) + } mock = () => { // Mock fetch if (this.original === null) { - this.original = global.fetch; + this.original = global.fetch global.fetch = jest.fn(async (url, options) => { - await delay(500); + await delay(500) if (this.options.throwError !== false) { - throw this.options.throwError; + throw this.options.throwError } try { return new Response( JSON.stringify({ tested: true, url }), this.options.responseOptions.bind(this)() - ); + ) } catch (err) { - console.error("MOCKED FETCH ERROR:", err); - throw err; + console.error('MOCKED FETCH ERROR:', err) + throw err } - }); + }) } - }; + } unmock = () => { // Unmock fetch - global.fetch = this.original; - this.original = null; - }; + global.fetch = this.original + this.original = null + } } class AbortError extends Error { - constructor(...args) { - super(...args); - this.name = "AbortError"; + constructor (...args) { + super(...args) + this.name = 'AbortError' } } @@ -111,5 +111,5 @@ export { catchConsoleLog, catchConsoleDir, MockFetch, - AbortError, -}; + AbortError +} diff --git a/app/src/tests/App.int.test.js b/app/src/tests/App.int.test.js index ec4519da..1b357d90 100644 --- a/app/src/tests/App.int.test.js +++ b/app/src/tests/App.int.test.js @@ -1,59 +1,59 @@ -import React from "react"; -import { render, fireEvent, act } from "@testing-library/react"; +import React from 'react' +import { render, fireEvent, act } from '@testing-library/react' -import "../testSetup"; -import App from "../App"; +import '../testSetup' +import App from '../App' -jest.mock("../Backend", () => { +jest.mock('../Backend', () => { return { __esModule: true, BackendProvider: ({ children }) => <>{children}</>, - IndexesHandler: ({ children }) => <>{children}</>, - }; -}); + IndexesHandler: ({ children }) => <>{children}</> + } +}) -jest.mock("../Widget", () => { +jest.mock('../Widget', () => { return { __esModule: true, - WidgetsList: () => <div>WidgetsList</div>, - }; -}); + WidgetsList: () => <div>WidgetsList</div> + } +}) -test("renders copyright link", () => { - const { getByText } = render(<App onLoadNewServiceWorkerAccept={() => {}} />); - const linkElement = getByText(/Source code of/i); - expect(linkElement).toBeInTheDocument(); -}); +test('renders copyright link', () => { + const { getByText } = render(<App onLoadNewServiceWorkerAccept={() => {}} />) + const linkElement = getByText(/Source code of/i) + expect(linkElement).toBeInTheDocument() +}) -test("menu shows detected new service worker", async () => { - let app; +test('menu shows detected new service worker', async () => { + let app act(() => { - app = render(<App onLoadNewServiceWorkerAccept={() => {}} />); - }); + app = render(<App onLoadNewServiceWorkerAccept={() => {}} />) + }) await act(async () => { - const event = new CustomEvent("onNewServiceWorker", { - detail: { registration: true }, - }); - fireEvent(document, event); - const updateElement = await app.findByText("1"); // Detect menu badge - expect(updateElement).toBeInTheDocument(); - }); -}); + const event = new CustomEvent('onNewServiceWorker', { + detail: { registration: true } + }) + fireEvent(document, event) + const updateElement = await app.findByText('1') // Detect menu badge + expect(updateElement).toBeInTheDocument() + }) +}) -test("translates app into detected language", async () => { - const languageGetter = jest.spyOn(window.navigator, "language", "get"); +test('translates app into detected language', async () => { + const languageGetter = jest.spyOn(window.navigator, 'language', 'get') await act(async () => { - languageGetter.mockReturnValue("en-US"); - const app = render(<App onLoadNewServiceWorkerAccept={() => {}} />); - const [codeElement] = await app.findAllByText("Refactored"); - expect(codeElement).toBeInTheDocument(); + languageGetter.mockReturnValue('en-US') + const app = render(<App onLoadNewServiceWorkerAccept={() => {}} />) + const [codeElement] = await app.findAllByText('Refactored') + expect(codeElement).toBeInTheDocument() // Ensure app is fully re-created to re-run the constructor - app.unmount(); - }); + app.unmount() + }) await act(async () => { - languageGetter.mockReturnValue("ca-ES"); - const app = render(<App onLoadNewServiceWorkerAccept={() => {}} />); - const [codeElement] = await app.findAllByText("Refactoritzada"); - expect(codeElement).toBeInTheDocument(); - }); -}); + languageGetter.mockReturnValue('ca-ES') + const app = render(<App onLoadNewServiceWorkerAccept={() => {}} />) + const [codeElement] = await app.findAllByText('Refactoritzada') + expect(codeElement).toBeInTheDocument() + }) +})