Skip to content

Commit

Permalink
feat: add context selector
Browse files Browse the repository at this point in the history
  • Loading branch information
Noggling committed Oct 30, 2024
1 parent 4648148 commit adc6597
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { User } from './user/UserCard';
import { useNavigateOnContextChange } from '../hooks/use-navigate-on-context-change';
import { NavigationModule } from '@equinor/fusion-framework-module-navigation';
import { useFramework } from '@equinor/fusion-framework-react';
import { PortalContextSelector } from '../context/PortalContextSelector';
import { ContextProvider } from '../context/ContextProvider';

// const styles = {
// contentSection: css`
Expand Down Expand Up @@ -125,10 +127,10 @@ export const Styles = {

export const ProjectPortalPage = (): JSX.Element => {
const [value, setValue] = useState(false);
const { data, isLoading } = useUserOrgDetails(value);
const { modules } = useFramework<[NavigationModule]>();
const { feature } = useFeature('project-prediction');
const { data, isLoading } = useUserOrgDetails(value);
useNavigateOnContextChange();
const { modules } = useFramework<[NavigationModule]>();
return (
<Styles.Wrapper>
<ProjectHeader>
Expand All @@ -145,9 +147,9 @@ export const ProjectPortalPage = (): JSX.Element => {
selected context through the menu.
</Typography>
</Styles.Section>
{/* <ContextProvider>
<ContextProvider>
<PortalContextSelector />
</ContextProvider> */}
</ContextProvider>
</Styles.ContentWrapper>
{feature?.enabled && (
<Styles.Padding>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ContextProvider as FusionContextProvider } from '@equinor/fusion-react-context-selector';
import { ReactNode } from 'react';
import { useContextResolver } from './hooks/use-context-resolver';

interface PortalContextProviderProps {
children: ReactNode;
}

export const ContextProvider = ({ children }: PortalContextProviderProps) => {
const resolver = useContextResolver(['ProjectMaster', 'Facility']);

return <FusionContextProvider resolver={resolver}>{children}</FusionContextProvider>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useContextProvider } from '@equinor/fusion-framework-react-app/context';
import FusionContextSelector, { ContextResult, ContextSelectEvent } from '@equinor/fusion-react-context-selector';
import { NavigateFunction } from 'react-router-dom';

interface ContextSelectorProps {
variant?: string;
navigate?: NavigateFunction;
}

export const ContextSelector = ({ variant }: ContextSelectorProps) => {
const contextProvider = useContextProvider();

return (
<FusionContextSelector
id="context-selector"
variant={variant}
onSelect={(e: ContextSelectEvent) => {
e.stopPropagation();
// sins this is a single select the will be the next context at index 0
const context = (e.nativeEvent.detail.selected as ContextResult)[0];
contextProvider.contextClient.setCurrentContext(context.id);
}}
value={contextProvider.currentContext?.id ? contextProvider.currentContext?.title || '' : ''}
placeholder="Start to type to search..."
selectTextOnFocus={true}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button } from '@equinor/eds-core-react';

import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';

import { ContextSelector } from './ContextSelector';
import { useContextProvider } from '@equinor/fusion-framework-react-app/context';
import { getContextPageURL } from '../hooks/utils';
import { NavigationModule } from '@equinor/fusion-framework-module-navigation';
import { useFramework } from '@equinor/fusion-framework-react';

const StyledWrapper = styled.div`
display: flex;
width: 50vw;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 0.5rem;
> fwc-searchable-dropdown-provider {
flex: 1;
}
@media only screen and (max-width: 60rem) {
width: 80vw;
}
@media only screen and (max-width: 45rem) {
width: 90vw;
flex-direction: column;
}
`;

const StyledButton = styled(Button)`
white-space: nowrap;
`;
const StyledActionWrapper = styled.div`
min-width: 120px;
`;

export const PortalContextSelector = () => {
const { currentContext } = useContextProvider();
const { modules } = useFramework<[NavigationModule]>();

return (
<StyledWrapper>
<ContextSelector />
<StyledActionWrapper>
{currentContext && (
<StyledButton
variant="ghost"
onClick={() => {
modules.navigation.replace(getContextPageURL(currentContext));
}}
>
Go to {currentContext.title}
</StyledButton>
)}
</StyledActionWrapper>
</StyledWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react';

import {
ClientMethodType,
ServicesModule,
} from '@equinor/fusion-framework-module-services';
import { ContextApiClient } from '@equinor/fusion-framework-module-services/context';
import { useFramework } from '@equinor/fusion-framework-react';

export const useContextClient = <T extends ClientMethodType>(
type: T
): ContextApiClient<T> | null => {
const [client, setClient] = useState<ContextApiClient<T> | null>(null);

const { modules } = useFramework<[ServicesModule]>();

useEffect(() => {
modules.services.createContextClient(type).then(setClient);
}, [modules.services, setClient, type]);

return client;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { QueryContextResponse } from '@equinor/fusion-framework-module-services/context/query';
import { ContextResolver, ContextResult, ContextResultItem } from '@equinor/fusion-react-context-selector';
import { useCallback } from 'react';

// import { clearLocalContext } from '../framework-configurator';
import { useContextProvider } from '@equinor/fusion-framework-react-app/context';
import { getContextHistory } from '../portal-context-history';
import { useContextClient } from './use-context-client';

export const useContextResolver = (type: string[]): ContextResolver => {
const contextProvider = useContextProvider();

const client = useContextClient('json');
const minQueryLength = 2;

const searchQuery = useCallback(
async (search: string): Promise<ContextResult> => {
let searchResult: ContextResult = [];
if (!client) {
return [
singleItem({
title: 'Client Error',
subTitle: 'No client provided to framework',
isDisabled: true,
isError: true,
}),
];
}

try {
if (!search || search.length < minQueryLength) {
searchResult.push(
singleItem({
title: `Need ${minQueryLength - search.length} more chars`,
isDisabled: true,
})
);
return searchResult;
}

const contexts = await client.query('v1', {
query: { search, filter: { type } },
});

if (contexts[0] && !contexts[0].id) return searchResult;
// Structure as type

searchResult = type.length > 1 ? contextResultMappedByTypes(contexts) : contextResultMapped(contexts);

if (searchResult.length === 0) {
searchResult.push(singleItem({ title: 'No matches...', isDisabled: true }));
}

return searchResult;
} catch (e) {
return [
singleItem({
title: 'API Error',
subTitle: e,
isDisabled: true,
isError: true,
}),
];
}
},
[client, type]
);

const children = getContextHistory(type);

const historyItems = {
id: 'history',
title: 'History',
type: 'section',
children,
};

return {
searchQuery,
initialResult: children.length > 0 ? [singleItem(historyItems)] : [],
closeHandler: (e: MouseEvent) => {
e.stopPropagation();
contextProvider.clearCurrentContext();
// clearLocalContext();
},
};
};

const singleItem = (props: unknown): ContextResultItem => {
return Object.assign({ id: '0', title: 'Dummy title' }, props);
};

function contextResultMappedByTypes(contexts: QueryContextResponse<'v1'>): ContextResult {
return contexts.reduce((result, context) => {
const index = result.findIndex((r) => r.title === context.type.id);
if (index === -1) {
result.push(
singleItem({
id: context.type.id,
title: context.type.id,
type: 'section',
children: [singleItem({ id: context.id, title: context.title || '', subTitle: context.type?.id })],
})
);
return result;
}

result[index].children?.push(
singleItem({
id: context.id,
title: context.title || '',
subTitle: context.type?.id,
})
);

return result;
}, [] as ContextResult);
}

function contextResultMapped(contexts: QueryContextResponse<'v1'>): ContextResult {
return contexts.map((context) =>
singleItem({
id: context.id,
title: context.title || '',
subTitle: context.type?.id,
})
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IContextProvider } from '@equinor/fusion-framework-module-context';

import { setContextHistory } from './portal-context-history';
import { storage } from '../utils/local-storage';

const CONTEXT_SOCAGE_KEY = 'context';

export function storeCurrentContext(contextProvider: IContextProvider) {
contextProvider.currentContext$.subscribe((context) => {
if (context?.title === 'Unexpected error' || !context?.id || !context) {
return;
}

const storedContextId = storage.getItem<string>(CONTEXT_SOCAGE_KEY);
// Update the history with the current context selected.
setContextHistory(context);

if (context.id !== storedContextId) {
storage.setItem(CONTEXT_SOCAGE_KEY, context?.id);
}
});
}

export function clearLocalContext() {
storage.removeItem(CONTEXT_SOCAGE_KEY);
}

export function validateLocalContext(contextId: string): boolean {
const storedContextId = storage.getItem<string>(CONTEXT_SOCAGE_KEY);
return contextId === storedContextId;
}

export function setStoredContext(contextProvider: IContextProvider) {
const storedContextId = storage.getItem<string>(CONTEXT_SOCAGE_KEY);

const uriContext = getContextFormUrl();

if (contextProvider.currentContext?.id !== storedContextId || uriContext) {
contextProvider.contextClient.setCurrentContext(uriContext ? uriContext : storedContextId);
}
}

export function getContextFormUrl() {
const match = window.location.pathname.match(
/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/
);
return match ? match[0] : undefined;
}
Loading

0 comments on commit adc6597

Please sign in to comment.