diff --git a/examples/multistep-form/src/App.jsx b/examples/multistep-form/src/App.jsx index cc1383db..81f7e373 100644 --- a/examples/multistep-form/src/App.jsx +++ b/examples/multistep-form/src/App.jsx @@ -5,9 +5,9 @@ import HomePage from "./pages/Home"; import { Alert, Button, Link, GridContainer } from "@trussworks/react-uswds"; import { Application } from "@nmfs-radfish/react-radfish"; -function App() { +function App({ application }) { return ( - +

Multi-Step

diff --git a/examples/multistep-form/src/index.jsx b/examples/multistep-form/src/index.jsx index 63bd0dd6..a9d93059 100644 --- a/examples/multistep-form/src/index.jsx +++ b/examples/multistep-form/src/index.jsx @@ -1,26 +1,26 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./styles/theme.css"; -import App from "./App"; -import { ErrorBoundary, OfflineStorageWrapper } from "@nmfs-radfish/react-radfish"; +import MultiStepFormApplication from "./App"; +import { Application, IndexedDBMethod } from "@nmfs-radfish/radfish"; +import { ErrorBoundary } from "@nmfs-radfish/react-radfish"; -const offlineStorageConfig = { - type: "indexedDB", - name: import.meta.env.VITE_INDEXED_DB_NAME, - version: import.meta.env.VITE_INDEXED_DB_VERSION, - stores: { - formData: "uuid, fullName, email, city, state, zipcode", - }, -}; +const app = new Application({ + storage: new IndexedDBMethod( + import.meta.env.VITE_INDEXED_DB_NAME, + import.meta.env.VITE_INDEXED_DB_VERSION, + { + formData: "uuid, fullName, email, city, state, zipcode", + }, + ), +}); const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - - - + , ); diff --git a/examples/on-device-storage/src/App.jsx b/examples/on-device-storage/src/App.jsx index 470c58ef..f89917e3 100644 --- a/examples/on-device-storage/src/App.jsx +++ b/examples/on-device-storage/src/App.jsx @@ -4,9 +4,10 @@ import { Routes, Route, BrowserRouter as Router } from "react-router-dom"; import { Application } from "@nmfs-radfish/react-radfish"; import HomePage from "./pages/Home"; -const App = () => { +const App = ({application}) => { + return ( - +
diff --git a/examples/on-device-storage/src/index.jsx b/examples/on-device-storage/src/index.jsx index 02a7673b..5de30777 100644 --- a/examples/on-device-storage/src/index.jsx +++ b/examples/on-device-storage/src/index.jsx @@ -2,11 +2,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./styles/theme.css"; import App from "./App"; -import { OfflineStorageWrapper } from "@nmfs-radfish/react-radfish"; import { ErrorBoundary } from "@nmfs-radfish/react-radfish"; +import { Application, IndexedDBMethod } from "@nmfs-radfish/radfish"; const offlineStorageConfig = { - type: "indexedDB", name: import.meta.env.VITE_INDEXED_DB_NAME, version: import.meta.env.VITE_INDEXED_DB_VERSION, stores: { @@ -17,14 +16,22 @@ const offlineStorageConfig = { }, }; +const app = new Application({ + storage: new IndexedDBMethod( + offlineStorageConfig.name, + offlineStorageConfig.version, + offlineStorageConfig.stores, + ), +}); + const root = ReactDOM.createRoot(document.getElementById("root")); -root.render( - - - - - - - -); +app.on("ready", () => { + root.render( + + + + + , + ); +}); diff --git a/examples/simple-table/src/index.jsx b/examples/simple-table/src/index.jsx index 632831dc..c83b1f59 100644 --- a/examples/simple-table/src/index.jsx +++ b/examples/simple-table/src/index.jsx @@ -2,25 +2,13 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "./styles/theme.css"; import App from "./App"; -import { ErrorBoundary, OfflineStorageWrapper } from "@nmfs-radfish/react-radfish"; - -const offlineStorageConfig = { - type: "indexedDB", - name: import.meta.env.VITE_INDEXED_DB_NAME, - version: import.meta.env.VITE_INDEXED_DB_VERSION, - stores: { - formData: "uuid, image, species, computedPrice, isDraft", - }, -}; - +import { ErrorBoundary } from "@nmfs-radfish/react-radfish"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - - - + , ); diff --git a/packages/radfish/Application.spec.js b/packages/radfish/Application.spec.js new file mode 100644 index 00000000..089f80fd --- /dev/null +++ b/packages/radfish/Application.spec.js @@ -0,0 +1,69 @@ +import { Application, IndexedDBMethod, LocalStorageMethod } from './index'; + +describe ('Application', () => { + describe('storage', () => { + it('should return the storage method', () => { + // IndexedDB Storage application + const indexedDBMethod = new IndexedDBMethod( + "test", + 1, + { + formData: "uuid, fullName, email, phoneNumber, numberOfFish, species, computedPrice, isDraft", + species: "name, price", + homebaseData: "KEY, REPORT_TYPE, SORT_KEY, TRIP_TYPE, VALUE", + }, + ); + const indexedDBApplication = new Application({ + storage: indexedDBMethod, + }); + expect (indexedDBApplication.storage).toEqual(indexedDBMethod); + + // Local Storage application + const localStorageMethod = new LocalStorageMethod( + "test", + { + formData: "uuid, fullName, email, phoneNumber, numberOfFish, species, computedPrice, isDraft", + species: "name, price", + homebaseData: "KEY, REPORT_TYPE, SORT_KEY, TRIP_TYPE, VALUE", + }, + ); + const localStorageApplication = new Application({ + storage: localStorageMethod, + }); + expect(localStorageApplication.storage).toEqual(localStorageMethod); + }); + + it('should return the storage method using a configuration object', function () { + const indexedDBApplication = new Application( + { + storage: { + type: "indexedDB", + name: "test", + version: 1, + stores: { + formData: "uuid, fullName, email, phoneNumber, numberOfFish, species, computedPrice, isDraft", + species: "name, price", + homebaseData: "KEY, REPORT_TYPE, SORT_KEY, TRIP_TYPE, VALUE", + } + } + } + ) + expect(indexedDBApplication.storage).toBeInstanceOf(IndexedDBMethod); + + const localStorageApplication = new Application( + { + storage: { + type: "localStorage", + name: "test", + stores: { + formData: "uuid, fullName, email, phoneNumber, numberOfFish, species, computedPrice, isDraft", + species: "name, price", + homebaseData: "KEY, REPORT_TYPE, SORT_KEY, TRIP_TYPE, VALUE", + } + } + } + ) + expect(localStorageApplication.storage).toBeInstanceOf(LocalStorageMethod); + }); + }); +}); \ No newline at end of file diff --git a/packages/radfish/index.js b/packages/radfish/index.js index efae485f..62d8fc21 100644 --- a/packages/radfish/index.js +++ b/packages/radfish/index.js @@ -1,4 +1,5 @@ import { setupWorker } from "msw/browser"; +import { StorageMethod, IndexedDBMethod, LocalStorageMethod } from "./on-device-storage/storage"; class EventEmitter extends EventTarget {} @@ -14,6 +15,34 @@ export class Application { this._dispatch("init"); } + get storage() { + if (!this._options.storage) { + return null; + } + + if (!(this._options.storage instanceof StorageMethod)) { + console.warn('Please update the storage method to be an instance of StorageMethod'); + + switch (this._options.storage?.type) { + case "indexedDB": { + return new IndexedDBMethod( + this._options.storage.name, + this._options.storage.version, + this._options.storage.stores + ); + } + case "localStorage": { + return new LocalStorageMethod(this._options.storage.name); + } + default: { + throw new Error(`Invalid storage method type: ${this._options.storage.type}`); + } + } + } + + return this._options.storage; + } + on(event, callback) { return this.emitter.addEventListener(event, callback); } diff --git a/packages/radfish/on-device-storage/storage/LocalStorageMethod.js b/packages/radfish/on-device-storage/storage/LocalStorageMethod.js index c009130e..30344034 100644 --- a/packages/radfish/on-device-storage/storage/LocalStorageMethod.js +++ b/packages/radfish/on-device-storage/storage/LocalStorageMethod.js @@ -13,9 +13,11 @@ export class LocalStorageMethod extends StorageMethod { constructor(key) { super(); this.key = key; - this.store = - localStorage.getItem(this.key) || + if (!localStorage.hasOwnProperty(this.key)) { + console.warn(`Initializing local storage for key: ${this.key}`); localStorage.setItem(this.key, JSON.stringify([])); + }; + this.store = localStorage.getItem(this.key); } /** diff --git a/packages/radfish/vitest.config.js b/packages/radfish/vitest.config.js new file mode 100644 index 00000000..5c7108aa --- /dev/null +++ b/packages/radfish/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + }, +}); diff --git a/packages/react-radfish/Application/index.jsx b/packages/react-radfish/Application/index.jsx index 3a12a32d..b26846e6 100644 --- a/packages/react-radfish/Application/index.jsx +++ b/packages/react-radfish/Application/index.jsx @@ -1,6 +1,7 @@ import { Toast } from "../alerts"; import { createContext, useEffect, useContext, useRef } from "react"; import { useOfflineStatus, useToasts, dispatchToast } from "../hooks"; +import { OfflineStorageWrapper } from "../OfflineStorage"; const ApplicationContext = createContext(); @@ -31,12 +32,22 @@ function ApplicationComponent(props) { ); } -export function Application({ application, children }) { - return ( - - {children} - - ); +export function Application({ application = null, children }) { + if (application?.storage) { + return ( + + + {children} + + + ); + } else { + return ( + + {children} + + ); + } } export const useApplication = () => { diff --git a/packages/react-radfish/OfflineStorage/index.jsx b/packages/react-radfish/OfflineStorage/index.jsx index d9bc26b8..04680142 100644 --- a/packages/react-radfish/OfflineStorage/index.jsx +++ b/packages/react-radfish/OfflineStorage/index.jsx @@ -1,14 +1,19 @@ import { createContext, useContext } from "react"; -import { IndexedDBMethod, LocalStorageMethod, StorageModelFactory } from "@nmfs-radfish/radfish"; +import { StorageModelFactory } from "@nmfs-radfish/radfish"; +import { useApplication } from "../Application"; export const OfflineStorageContext = createContext(); -export const OfflineStorageWrapper = ({ children, config }) => { - const storageMethod = - config.type === "indexedDB" - ? new IndexedDBMethod(config.name, config.version, config.stores) - : new LocalStorageMethod(config.name); +export const OfflineStorageWrapper = ({ children }) => { + const application = useApplication(); + if (!application?.storage) { + throw new Error( + "OfflineStorageWrapper must be used within an Application component with a storage method configured.", + ); + } + + const storageMethod = application.storage; const storageModel = StorageModelFactory.createModel(storageMethod); function createOfflineData(tableName, data) { @@ -43,7 +48,9 @@ export const OfflineStorageWrapper = ({ children, config }) => { export const useOfflineStorage = () => { const context = useContext(OfflineStorageContext); if (!context) { - throw new Error("useOfflineStorage must be used within an OfflineStorageWrapper"); + throw new Error( + "useOfflineStorage must be used within an OfflineStorageWrapper. Please make sure a storage method has been configured.", + ); } return context; }; diff --git a/templates/react-javascript/package.json b/templates/react-javascript/package.json index 1f79d457..6726a3b6 100644 --- a/templates/react-javascript/package.json +++ b/templates/react-javascript/package.json @@ -25,7 +25,7 @@ "start": "vite", "build": "vite build", "prebuild": "rm -rf dist", - "test": "vitest --exclude './src/__tests__/e2e/**'", + "test": "echo \"Error: no test specified\" && exit 1", "test:e2e": "concurrently --kill-others --success first \"npm start\" \"vitest './src/__tests__/e2e/integration.e2e.test.jsx'\"", "lint": "eslint src", "lint:fix": "eslint --fix src", diff --git a/templates/react-javascript/src/App.jsx b/templates/react-javascript/src/App.jsx index 742095fc..d333f639 100644 --- a/templates/react-javascript/src/App.jsx +++ b/templates/react-javascript/src/App.jsx @@ -12,17 +12,17 @@ import { import HomePage from "./pages/Home"; -function App() { +function App({ application }) { const [isExpanded, setExpanded] = useState(false); return ( - + Skip to main content
diff --git a/templates/react-javascript/src/index.jsx b/templates/react-javascript/src/index.jsx index e38fae0c..71e429fa 100644 --- a/templates/react-javascript/src/index.jsx +++ b/templates/react-javascript/src/index.jsx @@ -18,10 +18,10 @@ const app = new Application({ }, }); -app.on("ready", () => { +app.on("ready", async () => { root.render( - + , ); });