diff --git a/drama-queen/package.json b/drama-queen/package.json index 8a3a4923..741489da 100644 --- a/drama-queen/package.json +++ b/drama-queen/package.json @@ -13,6 +13,7 @@ "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/material": "^5.14.6", + "@reduxjs/toolkit": "^1.9.5", "@tanstack/react-query": "^4.29.19", "@tanstack/react-query-devtools": "^4.33.0", "@types/memoizee": "^0.4.8", @@ -26,6 +27,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.1", + "redux-clean-architecture": "^3.7.2", "tsafe": "^1.6.4", "tss-react": "^4.9.0", "zod": "^3.21.4" diff --git a/drama-queen/src/bootstrap.tsx b/drama-queen/src/bootstrap.tsx index 5afabe02..888a7c4d 100644 --- a/drama-queen/src/bootstrap.tsx +++ b/drama-queen/src/bootstrap.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { initializeCore } from "core" +import { createCoreProvider } from "core"; import { createRoot } from "react-dom/client"; import { RouterProvider } from "react-router-dom"; import { type RoutingStrategy, createRouter } from "ui/routing/createRouter"; @@ -24,14 +24,15 @@ const { QueenApiProvider } = createQueenApiProvider({ apiUrl: import.meta.env.VITE_QUEEN_API_URL }); -const { CoreLoadingFallback } = initializeCore({ +const { CoreProvider } = createCoreProvider({ "apiUrl": import.meta.env.VITE_API_URL, "keycloakParams": { "url": import.meta.env.VITE_KEYCLOAK_URL, "clientId": import.meta.env.VITE_KEYCLOAK_CLIENT_ID, "realm": import.meta.env.VITE_KEYCLOAK_REALM, "origin": window.location.origin + import.meta.env.BASE_URL - } + }, + "redirectUrl": import.meta.env.VITE_REDIRECT_URL, }); const mount = ({ @@ -60,9 +61,9 @@ const mount = ({ */ - Loading} > + Loading} > - + ); return () => queueMicrotask(() => root.unmount()); diff --git a/drama-queen/src/core/core.ts b/drama-queen/src/core/core.ts deleted file mode 100644 index 8a577f42..00000000 --- a/drama-queen/src/core/core.ts +++ /dev/null @@ -1,35 +0,0 @@ - -import { createApiClient } from "./queenApi/createApiClient"; -import { createKeycloakClient } from "./keycloakClient/createKeycloakClient"; - -import * as loadingDataUsecase from "./usecases/loadingData"; - -export type CoreParams = { - apiUrl: string; - keycloakParams: { - url: string; - clientId: string; - realm: string; - origin?: string; - } -}; - -export async function initializeCore(params: CoreParams) { - - const { apiUrl, keycloakParams } = params; - - const oidc = await createKeycloakClient(keycloakParams); - - const queenApi = await createApiClient({ - apiUrl, - "getAccessToken": !oidc.isUserLoggedIn ? - (() => null) : - (() => oidc.getAccessToken()) - }); - - loadingDataUsecase.setContext({ - oidc, - queenApi - }); - -} \ No newline at end of file diff --git a/drama-queen/src/core/index.ts b/drama-queen/src/core/index.ts new file mode 100644 index 00000000..7b106814 --- /dev/null +++ b/drama-queen/src/core/index.ts @@ -0,0 +1,19 @@ +/* +NOTE: Only here do we export the API for a specific framework (here react). +In the rest of the core directory everything is agnostic to React +*/ +import { createReactApi } from "redux-clean-architecture/react"; +import { createCore } from "./setup"; +import { usecases } from "./usecases"; + +export const { + createCoreProvider, + selectors, + useCoreEvts, + useCoreExtras, + useCoreFunctions, + useCoreState +} = createReactApi({ + createCore, + usecases +}); \ No newline at end of file diff --git a/drama-queen/src/core/index.tsx b/drama-queen/src/core/index.tsx deleted file mode 100644 index 064f07ec..00000000 --- a/drama-queen/src/core/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ - -import { useReducer, useEffect, useState } from "react"; -import * as loadingData from "./usecases/loadingData"; -import { useEvent } from "ui/tools/useEvent"; -import { CoreParams, initializeCore as initializeCore_vanilla } from "./core"; - -export function useLoadingDataState(): loadingData.State { - - const [, forceUpdate] = useReducer(() => ({}), {}) - - useEffect( - () => { - - const handler = () => { - forceUpdate() - }; - - loadingData.$stateUpdated.attach(handler); - - return () => { - loadingData.$stateUpdated.detach(handler); - }; - - }, - [] - ); - - return loadingData.state; - -} - -export function useRegisterLoadingDataAction( - params: { - redirect: (params: {url: string; })=> void; - } -){ - - - const constRedirect = useEvent(params.redirect); - - useEffect( - () => { - - const handler = (params: { action: "redirect"; url: string; }) => { - if (params.action === "redirect") { - constRedirect({ "url": params.url }); - } - }; - - loadingData.$action.attach(handler); - - return () => { - loadingData.$action.detach(handler); - }; - - }, - [] - ); - -} - -export const loadingDataFunctions = loadingData.functions; - - -export function initializeCore( - params: CoreParams -) { - - const prCoreInitialized = initializeCore_vanilla(params); - - function CoreLoadingFallback( - props: { - children: React.ReactNode; - fallback: React.ReactNode; - } - ) { - - - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - prCoreInitialized.then(() => setIsReady(true)); - }, []); - - return ( - <> - {!isReady ? props.fallback : props.children} - - ); - } - - return { CoreLoadingFallback }; - -} diff --git a/drama-queen/src/core/setup.ts b/drama-queen/src/core/setup.ts new file mode 100644 index 00000000..8b61bbbf --- /dev/null +++ b/drama-queen/src/core/setup.ts @@ -0,0 +1,50 @@ + +import { createCoreFromUsecases } from "redux-clean-architecture"; +import type { GenericCreateEvt, GenericThunks } from "redux-clean-architecture"; +import { createApiClient } from "./queenApi/createApiClient"; +import { createKeycloakClient } from "./keycloakClient/createKeycloakClient"; +import { usecases } from "./usecases"; + +type CoreParams = { + apiUrl: string; + keycloakParams: { + url: string; + clientId: string; + realm: string; + origin?: string; + }; + redirectUrl: string; +}; + +export async function createCore(params: CoreParams) { + + const { apiUrl, keycloakParams } = params; + + const oidc = await createKeycloakClient(keycloakParams); + + const queenApi = createApiClient({ + apiUrl, + "getAccessToken": !oidc.isUserLoggedIn ? + (() => null) : + (() => oidc.getAccessToken()) + }); + + const core = createCoreFromUsecases({ + "thunksExtraArgument": { + "coreParams": params, + oidc, + queenApi + }, + usecases + }); + + return core; +} + +type Core = Awaited>; + +export type State = ReturnType; + +export type Thunks = GenericThunks; + +export type CreateEvt = GenericCreateEvt; \ No newline at end of file diff --git a/drama-queen/src/core/usecases/index.ts b/drama-queen/src/core/usecases/index.ts new file mode 100644 index 00000000..11f1ed30 --- /dev/null +++ b/drama-queen/src/core/usecases/index.ts @@ -0,0 +1,4 @@ + +import * as loadingData from "./loadingData"; + +export const usecases = { loadingData }; \ No newline at end of file diff --git a/drama-queen/src/core/usecases/loadingData.ts b/drama-queen/src/core/usecases/loadingData.ts index 956a5bcb..9ca8ff0a 100644 --- a/drama-queen/src/core/usecases/loadingData.ts +++ b/drama-queen/src/core/usecases/loadingData.ts @@ -1,9 +1,10 @@ +import type { Thunks, CreateEvt } from "../setup"; +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { State as RootState } from "../setup"; import { id } from "tsafe/id"; -import { createEventEmitter, NonPostable } from "../tools/EventEmitter"; -import type { QueenApi } from "../queenApi/QueenApi"; -import type { Oidc } from "../keycloakClient/Oidc"; -import { assert } from "tsafe/assert"; - +import { createSelector } from "@reduxjs/toolkit"; +import { Evt } from "evt"; export type State = State.NotRunning | State.Running; @@ -17,99 +18,161 @@ export namespace State { nomenclatureProgress: number; surveyProgress: number; }; -} - -export const state: State = id({ - "stateDescription": "not running" -}); - -const $stateUpdated_internal = createEventEmitter(); - -export const $stateUpdated: NonPostable = $stateUpdated_internal; - -const reducers = { - "updateProgresses": (params: { - surveyUnitProgress: number; - nomenclatureProgress: number; - surveyProgress: number; - }) => { - const { nomenclatureProgress, surveyProgress, surveyUnitProgress } = params; - - Object.assign(state, id({ - "stateDescription": "running", - surveyUnitProgress, - nomenclatureProgress, - surveyProgress - })); - $stateUpdated_internal.post(); - - } } -export const functions = { - "start": async () => { +export const name = "loadingData"; + +export const { reducer, actions } = createSlice({ + name, + "initialState": id({ + "stateDescription": "not running" + }), + "reducers": { + "progressUpdated": (state, { payload }: PayloadAction<{ + surveyUnitProgress: number; + nomenclatureProgress: number; + surveyProgress: number; + }>) => { + const { + nomenclatureProgress, + surveyProgress, + surveyUnitProgress + } = payload; + + return { + "stateDescription": "running", + nomenclatureProgress, + surveyProgress, + surveyUnitProgress + }; + }, + "completed": (state, { payload }: PayloadAction<{ + redirectUrl: string; + }>) => { + return { "stateDescription": "not running" } + }, + } +}); - if (id(state).stateDescription === "running") { - return; +export const thunks = { + "start": + () => + async (...args) => { + const [dispatch, getState, { coreParams }] = args; + + { + const state = getState()[name]; + + if (state.stateDescription === "running") { + return; + } + + } + + dispatch( + actions.progressUpdated({ + "nomenclatureProgress": 0, + "surveyProgress": 0, + "surveyUnitProgress": 0 + }) + ); + + for (const progress of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + + dispatch( + actions.progressUpdated({ + "nomenclatureProgress": progress, + "surveyProgress": progress, + "surveyUnitProgress": progress + }) + ); + + } + + dispatch( + actions.completed({ + "redirectUrl": coreParams.redirectUrl + }) + ); + + } +} satisfies Thunks; + +export const selectors = (() => { + const runningState = (rootState: RootState) => { + const state = rootState[name]; + + if (state.stateDescription === "not running") { + return undefined; } + return state; - const context = getContext(); - - reducers.updateProgresses({ - "nomenclatureProgress": 0, - "surveyProgress": 0, - "surveyUnitProgress": 0 - }); - - for (const progress of [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) { - await new Promise((resolve) => setTimeout(resolve, 1000)); + }; - reducers.updateProgresses({ - "nomenclatureProgress": progress, - "surveyProgress": progress, - "surveyUnitProgress": progress - }); + const isRunning = createSelector( + runningState, + state => state !== undefined + ); + + const surveyUnitProgress = createSelector( + runningState, + state => { + if (state === undefined) { + return undefined; + } + return state.surveyUnitProgress; } + ); + + const nomenclatureProgress = createSelector( + runningState, + state => { + if (state === undefined) { + return undefined; + } + return state.nomenclatureProgress; + } + ); + + const surveyProgress = createSelector( + runningState, + state => { + if (state === undefined) { + return undefined; + } + return state.surveyProgress; + } + ); - $action.post({ - "action": "redirect", - "url": window.document.location.origin - }); - - } -} - -const { getContext, setContext } = (() => { - - type Context = { - queenApi: QueenApi; - oidc: Oidc; + return { + isRunning, + surveyUnitProgress, + nomenclatureProgress, + surveyProgress }; - let context: Context | undefined = undefined; +})(); - function getContext() { - assert(context !== undefined, "not initialized"); - return context; - } - function setContext(context_: Context) { - context = context_; - } +export const createEvt = (({ evtAction }) =>{ - return { - getContext, - setContext - }; -})(); - -export { setContext }; + const evt = Evt.create<{ + action: "redirect"; + url: string; + }>(); + evtAction + .pipe(data => data.sliceName === name && data.actionName === "completed" ? [data.payload.redirectUrl] : null) + .attach((redirectUrl) => { -export const $action = createEventEmitter<{ - action: "redirect"; - url: string; -}>(); + evt.post({ + "action": "redirect", + "url": redirectUrl + }); + }); + return evt; +}) satisfies CreateEvt; \ No newline at end of file diff --git a/drama-queen/src/ui/pages/LoadingData.tsx b/drama-queen/src/ui/pages/LoadingData.tsx index 1ceb6042..93e2a0fc 100644 --- a/drama-queen/src/ui/pages/LoadingData.tsx +++ b/drama-queen/src/ui/pages/LoadingData.tsx @@ -2,37 +2,71 @@ import { useEffect, useReducer } from "react"; import * as loadingData from "core/usecases/loadingData"; import CircularProgress from "@mui/material/CircularProgress" import LinearProgress from '@mui/material/LinearProgress'; -import { useLoadingDataState, useRegisterLoadingDataAction, loadingDataFunctions } from "core"; +import { selectors, useCoreState, useCoreFunctions, useCoreEvts } from "core"; +import { assert } from "tsafe/assert" +import { useEvt } from "evt/hooks" export function LoadingData() { + /* + const { isRunning } = useCoreState(selectors.loadingData.isRunning); + const { nomenclatureProgress } = useCoreState(selectors.loadingData.nomenclatureProgress); + const { surveyProgress } = useCoreState(selectors.loadingData.surveyProgress); + const { surveyUnitProgress } = useCoreState(selectors.loadingData.surveyUnitProgress); + */ + + const loadingDataState = useCoreState(state=> state.loadingData); + + const { loadingData } = useCoreFunctions(); + useEffect( () => { - loadingDataFunctions.start(); + loadingData.start(); }, [] ); - useRegisterLoadingDataAction({ - "redirect": ({ url }) => { - alert("redirect to " + url) - } - }); + const { evtLoadingData } = useCoreEvts(); + + useEvt( + ctx=> { - const loadingDataState = useLoadingDataState(); + evtLoadingData.$attach( + data => data.action === "redirect" ? [data.url] : null, + ctx, + url => { + alert("redirect to " + url) + } + ); - if (loadingDataState.stateDescription === "not running") { + }, + [] + ); + + /* + if (!isRunning) { + return null; + } + + assert(nomenclatureProgress !== undefined); + assert(surveyProgress !== undefined); + assert(surveyUnitProgress !== undefined); + */ + + if( loadingDataState.stateDescription !== "running"){ return null; } + const { nomenclatureProgress, surveyProgress, surveyUnitProgress } = loadingDataState; + return (
- +
- +
- +
diff --git a/yarn.lock b/yarn.lock index f1dec9a3..f7632cf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,7 +1165,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== @@ -2485,6 +2485,16 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== +"@reduxjs/toolkit@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" + integrity sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ== + dependencies: + immer "^9.0.21" + redux "^4.2.1" + redux-thunk "^2.4.2" + reselect "^4.1.8" + "@remix-run/router@1.8.0": version "1.8.0" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc" @@ -8701,6 +8711,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immer@^9.0.21: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + immutable@^4.0.0: version "4.3.4" resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" @@ -14153,6 +14168,26 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redux-clean-architecture@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux-clean-architecture/-/redux-clean-architecture-3.7.2.tgz#b153e940bc4d239483e9f1bb6c75cc78923365ff" + integrity sha512-QLbZwex8+m8dmqXeAekE9Uzern3YVV1KBLYoPSt+R6z9hoNRACHEMjQaemXiAzlezRfqxLFeot9byK5SGfGxjw== + dependencies: + minimal-polyfills "^2.2.3" + tsafe "^1.6.4" + +redux-thunk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b" + integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== + +redux@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.getprototypeof@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3" @@ -14337,6 +14372,11 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== +reselect@^4.1.8: + version "4.1.8" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" + integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"