Skip to content

Commit

Permalink
feat: portal Menu
Browse files Browse the repository at this point in the history
  • Loading branch information
Noggling committed Nov 19, 2024
1 parent 64ae398 commit a78ede9
Show file tree
Hide file tree
Showing 19 changed files with 519 additions and 202 deletions.
13 changes: 13 additions & 0 deletions fusion-portal/module/menu/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@equinor/fusion-portal-module-menu",
"version": "0.0.1",
"license": "MIT",
"main": "./dist/src/index.js",
"module": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"scripts": {
"dev": "npm run build -- --watch",
"dev:local": "npm run dev -- --watch",
"build": "tsc -b -f"
}
}
13 changes: 13 additions & 0 deletions fusion-portal/module/menu/src/enablePortalMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { IModulesConfigurator } from '@equinor/fusion-framework-module';

import { menuModule } from './module';
/**
* Method for enabling the PortalMenu module
* @param configurator - configuration object
*/
export const enablePortalMenu = (configurator: IModulesConfigurator<any, any>): void => {
configurator.addConfig({
module: menuModule,
});
};
5 changes: 5 additions & 0 deletions fusion-portal/module/menu/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './provider';
export * from './module';
export { default } from './module';
export * from './enablePortalMenu';
export * from './use-portal-menu';
35 changes: 35 additions & 0 deletions fusion-portal/module/menu/src/local-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class LocalStorage {
setItem<T>(key: string, data: T): void {
/** Data is null or undefined */
if (!data) {
localStorage.removeItem(key);
return;
}

/** Data is already string */
if (typeof data === 'string') {
localStorage.setItem(key, data);
return;
}

//Anything else
localStorage.setItem(key, JSON.stringify(data));
}

getItem<T>(key: string): string | T | undefined {
const data = localStorage.getItem(key);
if (!data || data === 'undefined') return undefined;

try {
return JSON.parse(data) as T;
} catch {
return data;
}
}

removeItem(key: string): void {
localStorage.removeItem(key);
}
}

export const storage = new LocalStorage();
18 changes: 18 additions & 0 deletions fusion-portal/module/menu/src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Module } from '@equinor/fusion-framework-module';
import { IPortalMenuProvider, PortalMenuProvider } from './provider';

export type PortalMenuModule = Module<'menu', IPortalMenuProvider, unknown, []>;

export const menuModule: PortalMenuModule = {
name: 'menu',
initialize: ({ ref }) =>
ref?.menu ? (ref.menu as PortalMenuProvider) : new PortalMenuProvider(),
};

export default menuModule;

declare module '@equinor/fusion-framework-module' {
interface Modules {
menu: PortalMenuModule;
}
}
67 changes: 67 additions & 0 deletions fusion-portal/module/menu/src/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { FlowSubject, Observable } from '@equinor/fusion-observable';
import { createState } from './state/create-state';
import { Actions, actions } from './state/actions';
import { storage } from './local-storage';

export interface IPortalMenuProvider {
setSearchText(searchText: string): void;
toggleMenu<T>(e?: React.MouseEvent<T>): void;
closeMenu(): void;
state: MenuState;
state$: Observable<MenuState>;
}

export interface MenuState {
menuActive: boolean;
isLoading: boolean;
searchText: string;
}

const initialState: MenuState = {
menuActive: false,
isLoading: false,
searchText: '',
};

export const MENU_KEY = 'menuState';

export class PortalMenuProvider implements IPortalMenuProvider {
#state: FlowSubject<MenuState, Actions>;

get state(): MenuState {
return this.#state.value;
}

get state$(): Observable<MenuState> {
return this.#state.asObservable();
}

constructor() {
this.#state = createState(
(storage.getItem<MenuState>(MENU_KEY) as MenuState) || initialState,
this
);
}

toggleMenu = <T>(e?: React.MouseEvent<T>) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}

this.#state.next(actions.toggleMenu());
};

closeMenu = <T>(e?: React.MouseEvent<T>) => {
if (e) {
e.preventDefault();
e.stopPropagation();
}

this.#state.next(actions.closeMenu());
};

setSearchText = (searchText: string) => {
this.#state.next(actions.setSearchText(searchText));
};
}
27 changes: 27 additions & 0 deletions fusion-portal/module/menu/src/state/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ActionInstanceMap, ActionTypes, createAction, createAsyncAction } from '@equinor/fusion-observable';
import { MenuState } from '../provider';

const createActions = () => ({
/** Portal loading */
toggleMenu: createAction('toggle_menu', () => ({ payload: true })),
setSearchText: createAction('set_search_text', (searchText: string) => ({
payload: searchText,
})),
closeMenu: createAction('close_menu'),
trackState: createAsyncAction(
'track_state',
(payload: MenuState) => ({
payload,
}),
(state: MenuState) => ({ payload: state }),
(error: unknown) => ({ payload: error })
),
});

export const actions = createActions();

export type ActionBuilder = ReturnType<typeof createActions>;

export type ActionMap = ActionInstanceMap<ActionBuilder>;

export type Actions = ActionTypes<typeof actions>;
23 changes: 23 additions & 0 deletions fusion-portal/module/menu/src/state/create-reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createReducer as makeReducer } from '@equinor/fusion-observable';

import { enableMapSet } from 'immer';

enableMapSet();

import { actions } from './actions';
import { MenuState } from '../provider';

export const createReducer = (value: MenuState) =>
makeReducer({ ...value, status: new Set() } as MenuState, (builder) => {
builder.addCase(actions.toggleMenu, (state) => {
state.menuActive = !state.menuActive;
});
builder.addCase(actions.closeMenu, (state) => {
state.menuActive = false;
});
builder.addCase(actions.setSearchText, (state, action) => {
state.searchText = action.payload;
});
});

export default createReducer;
31 changes: 31 additions & 0 deletions fusion-portal/module/menu/src/state/create-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Flow, FlowSubject } from '@equinor/fusion-observable';

import { createReducer } from './create-reducer';

import { actions, type Actions } from './actions';

import { PortalMenuProvider, MenuState, MENU_KEY } from '../provider';
import { switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { storage } from '../local-storage';

export const createState = (
value: MenuState,
provider: PortalMenuProvider
): FlowSubject<MenuState, Actions> => {
const reducer = createReducer(value);
const state = new FlowSubject<MenuState, Actions>(reducer);
state.addFlow(myFlow(provider));
return state;
};

export const myFlow =
(provider: PortalMenuProvider): Flow<Actions, MenuState> =>
(action$) =>
action$.pipe(
switchMap(() => {
const state = provider.state;
storage.setItem(MENU_KEY, state);
return new Observable<Actions>((s) => s.next(actions.trackState(provider.state)));
})
);
19 changes: 19 additions & 0 deletions fusion-portal/module/menu/src/use-portal-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useAppModule } from '@equinor/fusion-framework-react-app';
import { PortalMenuModule } from './module';
import { useObservableState } from '@equinor/fusion-observable/react';

export const usePortalMenu = () => {
const menu = useAppModule<PortalMenuModule>('menu');

if (!menu) {
throw new Error('Menu module not found');
}
const { toggleMenu, closeMenu, setSearchText, state, state$ } = menu;
const menuState = useObservableState(state$, { initial: state }).value;
return {
...menuState,
toggleMenu,
closeMenu,
setSearchText,
};
};
19 changes: 19 additions & 0 deletions fusion-portal/module/menu/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"rootDir": "src",
"types": ["vite/client"]
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
],
"extends": "../../../tsconfig.base.json"
}
27 changes: 27 additions & 0 deletions fusion-portal/module/menu/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": ".",
"types": [
"node"
]
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"dist"
],
"include": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx"
]
}
3 changes: 2 additions & 1 deletion fusion-portal/react/extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@equinor/fusion-portal-react-components": "workspace:^",
"@equinor/fusion-portal-react-app": "workspace:^",
"@equinor/fusion-portal-module-app-config": "workspace:^",
"@equinor/fusion-portal-react-utils": "workspace:^"
"@equinor/fusion-portal-react-utils": "workspace:^",
"@equinor/fusion-portal-module-menu": "workspace:^"
}
}
Loading

0 comments on commit a78ede9

Please sign in to comment.