People components
Less than 1 minute
There are a set of people components: avatar, card, list item and selector.
Installation
npm install @equinor/fusion-react-person
Person Provider
In order to use these components you need to have PersonProvider wrapper.
<fwc-person-provider .resolver={resolver}>
<!-- will do default search -->
<fwc-person-search></fwc-person-search>
<fwc-person-provider .resolver={customResolver}>
<!-- only display person with title 'foobar', but images are resolved from parent provider -->
<fwc-person-search></fwc-person-search>
</fwc-person-provider>
</fwc-person-provider>
const controller: MainController;
const resolver: PersonResolver = {
search: (args) => controller.search(args),
getPhoto: (args) => controller.getPhoto(args)
}
const customResolver: PersonResolver = {
search: (args) => controller.search(args).filter(x => x.title === 'foobar'),
}
Links
Example code
Avatar
import { PersonAvatar } from '@equinor/fusion-react-person';
import { FlexGrid } from '../Styled';
/**
* Renders a page with two `PersonAvatar` components, one using an Azure ID and one using an email address (UPN).
*
* The persons are fetched from the `PersonProvider` component, which are implemented in the host (application portal)
*
* In this example the `PersonAvatar` will request the profile photo from the `PersonProvider` by either the `azureId` or `upn`.
*
* @returns A React element representing the Avatar page.
*/
export const AvatarPage = () => {
return (
<>
<h2>Avatar</h2>
<FlexGrid>
<PersonAvatar azureId="cbc6480d-12c1-467e-b0b8-cfbb22612daa" />
<PersonAvatar upn="handah@equinor.com" />
</FlexGrid>
</>
);
};
Card
import { PersonCard } from '@equinor/fusion-react-person';
import { FlexGrid } from '../Styled';
/**
* Renders a page with two `PersonCard` components, one with an `azureId` prop and one with a `upn` prop.
*
* The persons are fetched from the `PersonProvider` component, which are implemented in the host (application portal)
*
* In this example the `PersonCard` will request the profile photo from the `PersonProvider` by either the `azureId` or `upn`.
*/
export const CardPage = () => {
return (
<>
<h2>Card</h2>
<FlexGrid>
<PersonCard azureId="cbc6480d-12c1-467e-b0b8-cfbb22612daa" />
<PersonCard upn="handah@equinor.com" />
</FlexGrid>
</>
);
};
ListItem
import { PersonListItem } from '@equinor/fusion-react-person';
import { FlexGridColumn } from '../Styled';
import { useSearchPersons, type ApiPersonSearchResultV2 } from '../api/person-search';
const demoSearch = 'FOIT CON PDL';
/**
* Maps an `ApiPersonSearchResultV2` object to a `PersonListItem` component.
*
* Since the person object is provided, the `dataSource` property on `PersonListItem` used,
* this wil skip the request for resolving the provided person.
*
* @param person - The `ApiPersonSearchResultV2` object to map.
* @returns A `PersonListItem` component with the data from the `person` object.
*/
const mapPersonToListItem = (person: ApiPersonSearchResultV2) => (
<PersonListItem
key={person.azureUniqueId}
dataSource={{
azureId: person.azureUniqueId!,
name: person.name,
mail: person.mail,
jobTitle: person.jobTitle,
department: person.department,
mobilePhone: person.mobilePhone,
officeLocation: person.officeLocation,
upn: person.upn,
accountType: person.accountType,
}}
/>
);
/**
* Renders a page that displays a list of persons based on a search query.
*
* This component fetches and displays a list of persons using the `useSearchPersons` hook.
* If there is an error fetching the data, an error message is displayed.
* If the data is still being fetched, a loading message is displayed.
* Otherwise, the list of persons is rendered using the `mapPersonToListItem` function.
*
* This example will use a custom search query and map the results to a `PersonListItem` component.
*
* @returns A React component that renders the list of persons.
*/
export const ListItemPage = () => {
// Fetch and handle search results for persons
const { persons, error, isSearching } = useSearchPersons(demoSearch);
// Display error message if there was an issue fetching the data
if (error) {
return (
<div>
<p>
{error.name}: {error.message}
</p>
<pre>{JSON.stringify(error.data ?? error.cause, null, 2)}</pre>
</div>
);
}
// Display loading message while fetching data
if (isSearching) {
return <div>Fetching demo data for [{demoSearch}] ...</div>;
}
// Render the list of persons
return (
<>
<h2>PersonListItems ({demoSearch})</h2>
<FlexGridColumn>
{persons.map(mapPersonToListItem)}
{/*
// Alternative way of mapping components, where the host people resolver is used.
// Note this will cause the host to re-resolve each person and use the data from the host api.
persons.map(person => (<PersonListItem key={person.azureUniqueId} azureId={person.azureUniqueId} />))
*/}
</FlexGridColumn>
</>
);
};
Selector
import { useCallback, useReducer } from 'react';
import {
PersonInfo,
PersonListItem,
PersonSelect,
PersonSelectEvent,
} from '@equinor/fusion-react-person';
import { styled } from 'styled-components';
import { Button, Icon } from '@equinor/eds-core-react';
import { delete_to_trash } from '@equinor/eds-icons';
Icon.add({ delete_to_trash });
import { createReducer, createAction } from '@equinor/fusion-observable';
const Story = styled.article`
margin: 3em 0;
`;
/** Actions for reducer cases */
const actions = {
add: createAction('add_selected', (person: PersonInfo) => ({ payload: person })),
remove: createAction('remove_person', (id: string) => ({ payload: id })),
clear: createAction('clear_selected'),
};
/** initial users to display in reducer */
const initial: Record<string, PersonInfo> = {
'f59e967d-8422-41ae-9000-a47f3ac0b70c': {
mail: 'ola@equinor.com',
name: 'Ola Nordman',
jobTitle: 'Prin Analyst Digital SW Arch',
department: 'FOS FOIT PDP',
mobilePhone: '+47 55555555',
officeLocation: 'ST-FV E3',
upn: 'ola@equinor.com',
accountType: 'Employee',
azureId: 'f59e967d-8422-41ae-9000-a47f3ac0b70c',
},
'cbc6480d-12c1-467e-9000-cfbb22612daa': {
mail: 'per@equinor.com',
name: 'Per Person',
jobTitle: 'X-Bouvet ASA (PX)',
department: 'FOIT CON PDP',
mobilePhone: '+47 55555555',
upn: 'per@equinor.com',
accountType: 'Consultant',
azureId: 'cbc6480d-12c1-467e-9000-cfbb22612daa',
},
};
/** Reducer attched to useReducer hook, */
const reducer = createReducer(initial, (builder) => {
builder.addCase(actions.add, (state, action) => {
state[action.payload.azureId] = action.payload;
});
builder.addCase(actions.remove, (state, action) => {
delete state[action.payload];
});
builder.addCase(actions.clear, () => {});
});
export const SelectorPage = () => {
/** Init reducer */
const [selected, dispatch] = useReducer(reducer, reducer.getInitialState());
const selectPersonCallback = useCallback(
(e: PersonSelectEvent) => {
const { selected: sel } = e.nativeEvent.detail;
if (sel) {
console.log('Selecting =>', sel);
dispatch(actions.add(sel));
}
},
[dispatch],
);
return (
<>
<h2>PersonSelect Component</h2>
<section>
<Story>
<p>Standard</p>
<PersonSelect placeholder="Search for person"></PersonSelect>
</Story>
<Story>
<p>Property: selectedPerson upn</p>
<PersonSelect selectedPerson={'handah@equinor.com'}></PersonSelect>
</Story>
<Story>
<p>Property: selectedPerson azureId</p>
<PersonSelect
selectedPerson={'cbc6480d-12c1-467e-b0b8-cfbb22612daa'}
></PersonSelect>
</Story>
<Story>
<p>Controlled component:</p>
<PersonSelect
selectedPerson={null}
placeholder="Search for person"
onSelect={selectPersonCallback}
></PersonSelect>
<p>Selected persons:</p>
<ul
style={{
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gridGap: '1em',
listStyle: 'none',
padding: 0,
}}
>
{Object.values(selected).map((person) => (
<li key={person.azureId}>
<PersonListItem dataSource={person}>
<Button
variant="ghost_icon"
onClick={() => dispatch(actions.remove(person.azureId))}
>
<Icon name="delete_to_trash" />
</Button>
</PersonListItem>
</li>
))}
</ul>
</Story>
</section>
</>
);
};