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"