diff --git a/.changeset/beige-rats-admire.md b/.changeset/beige-rats-admire.md new file mode 100644 index 000000000..d6ece1962 --- /dev/null +++ b/.changeset/beige-rats-admire.md @@ -0,0 +1,7 @@ +--- +"fusion-project-portal": minor +--- + + +Added `@portal/components` project for ui components utilising components and functionality form `@portal/ui` and `@portal/core`. + diff --git a/.changeset/cyan-snakes-flow.md b/.changeset/cyan-snakes-flow.md new file mode 100644 index 000000000..b91bc5b87 --- /dev/null +++ b/.changeset/cyan-snakes-flow.md @@ -0,0 +1,5 @@ +--- +"fusion-project-portal": minor +--- + +Added `@portal/ui` project for ui components used in the portal. diff --git a/.changeset/dry-beans-cheat.md b/.changeset/dry-beans-cheat.md new file mode 100644 index 000000000..c3297c439 --- /dev/null +++ b/.changeset/dry-beans-cheat.md @@ -0,0 +1,7 @@ +--- +"fusion-project-portal": patch +--- + + +Added `@portal/utils` project for utile funtions and moved old utils to new project. `@equinor/portal-utils` has now been deleted. + diff --git a/.changeset/nasty-jokes-wonder.md b/.changeset/nasty-jokes-wonder.md new file mode 100644 index 000000000..efc8948a7 --- /dev/null +++ b/.changeset/nasty-jokes-wonder.md @@ -0,0 +1,7 @@ +--- +"fusion-project-portal": minor +--- + + +Added `@portal/core` project for core functionality. Added the new functionality, and is now ready for migration. + diff --git a/.changeset/pr-434-1588251794.md b/.changeset/pr-434-1588251794.md new file mode 100644 index 000000000..1f14a2802 --- /dev/null +++ b/.changeset/pr-434-1588251794.md @@ -0,0 +1,5 @@ + +--- +"fusion-project-portal": minor +--- +Adding user info side-sheet with contact details, riles and my allocations. diff --git a/.changeset/pretty-onions-teach.md b/.changeset/pretty-onions-teach.md new file mode 100644 index 000000000..895590148 --- /dev/null +++ b/.changeset/pretty-onions-teach.md @@ -0,0 +1,5 @@ +--- +"fusion-project-portal": minor +--- + +Added `@portal/types` porject for globale portal types. diff --git a/client/package.json b/client/package.json index 7e1c04a09..d3de41a38 100644 --- a/client/package.json +++ b/client/package.json @@ -16,7 +16,7 @@ "private": true, "devDependencies": { "@hirez_io/observer-spy": "^2.2.0", - "@testing-library/jest-dom": "^5.17.0", + "@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1^", "@types/dompurify": "^3.0.2", @@ -71,9 +71,11 @@ "@equinor/fusion-framework-react": "^5.3.2", "@equinor/fusion-framework-react-app": "^4.1.9", "@equinor/fusion-framework-react-components-bookmark": "0.2.10", + "@equinor/fusion-framework-react-components-people-provider": "^1.1.2", "@equinor/fusion-framework-react-module-signalr": "^2.0.10", "@equinor/fusion-observable": "^8.1.2", "@equinor/fusion-react-context-selector": "^0.4.9", + "@equinor/fusion-react-person": "^0.6.0", "@equinor/fusion-react-side-sheet": "1.0.2", "@equinor/fusion-react-skeleton": "^0.2.14", "@equinor/fusion-react-styles": "^0.5.11", @@ -96,4 +98,4 @@ "workspaces": [ "packages/**" ] -} \ No newline at end of file +} diff --git a/client/packages/components/README.md b/client/packages/components/README.md new file mode 100644 index 000000000..97af14674 --- /dev/null +++ b/client/packages/components/README.md @@ -0,0 +1,7 @@ +# portal-ui + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test portal-ui` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/client/packages/components/package.json b/client/packages/components/package.json new file mode 100644 index 000000000..931343408 --- /dev/null +++ b/client/packages/components/package.json @@ -0,0 +1,10 @@ +{ + "name": "@portal/components", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "build": "tsc", + "test": "vitest --silent --run", + "test:coverage": "vitest run --coverage" + } +} \ No newline at end of file diff --git a/client/packages/components/src/components/my-account/MyAccount.tsx b/client/packages/components/src/components/my-account/MyAccount.tsx new file mode 100644 index 000000000..fd02ec0ef --- /dev/null +++ b/client/packages/components/src/components/my-account/MyAccount.tsx @@ -0,0 +1,100 @@ +import { useCurrentUser } from '@portal/core'; +import { getAccountTypeColor } from '@portal/ui'; + +import { SideSheet } from '@equinor/fusion-react-side-sheet'; + +import { ProfileCardHeader } from './components/ProfileCardHeader'; + +import { ProfileContactDetails } from './components/ProfileContactDetails'; +import { Button, Icon } from '@equinor/eds-core-react'; +import { ProfileManagerCard } from './components/ProfileManager'; +import { briefcase, settings, verified_user } from '@equinor/eds-icons'; +import styled from 'styled-components'; +import { tokens } from '@equinor/eds-tokens'; +import { useMemo, useState } from 'react'; +import { MyRolesTab } from '../my-roles-tab/MyRolesTab'; +import { MyAllocationTab } from '../my-allocations-tab/MyAllocationTab'; +import { PortalSettingsTab } from '../portal-settings-tab/PortalSettingsTab'; +import { PortalActionProps } from '@equinor/portal-core'; +import { PresenceIndicator } from '../presence-indicator'; + +const Style = { + Wrapper: styled.div` + padding: 1rem 0rem; + display: flex; + flex-direction: column; + align-items: flex-start; + `, + Button: styled(Button)` + width: 100%; + display: flex; + padding: 1rem; + height: auto; + `, +}; + +type Tabs = { + Profile: JSX.Element; + MyRoles: JSX.Element; + MyAllocations: JSX.Element; + PortalSettings: JSX.Element; +}; + +export function MyAccount({ action, onClose, open }: PortalActionProps) { + const { data: user, isLoading } = useCurrentUser(); + + const [activeTab, setActiveTab] = useState('Profile'); + + const tabs: Tabs = useMemo(() => { + return { + Profile: ( + <> + +
+ +
+ +
+ + setActiveTab('MyAllocations')}> + + My Allocations + + setActiveTab('MyRoles')}> + + My Roles + + +
+ + setActiveTab('PortalSettings')}> + + Portal Setting + + + + ), + MyRoles: setActiveTab('Profile')} user={user} />, + MyAllocations: setActiveTab('Profile')} positions={user?.positions} />, + PortalSettings: setActiveTab('Profile')} user={user} />, + }; + }, [user, setActiveTab]); + + return ( + + + + + + {/* This functionality is not yet implemented. */} + +
+ {tabs[activeTab]} +
+
+ ); +} diff --git a/client/packages/components/src/components/my-account/components/ProfileCardHeader.tsx b/client/packages/components/src/components/my-account/components/ProfileCardHeader.tsx new file mode 100644 index 000000000..99a4ff992 --- /dev/null +++ b/client/packages/components/src/components/my-account/components/ProfileCardHeader.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components'; +import { Typography } from '@equinor/eds-core-react'; +import { getDepartment, getJobTitle, useUserPhoto } from '@portal/core'; +import { PersonDetails } from '@portal/types'; +import { Avatar, Skeleton, getAccountTypeColor } from '@portal/ui'; + +const Style = { + InfoWrapper: styled.div` + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + `, +}; + +export const ProfileCardHeader = ({ user }: { user?: PersonDetails }) => { + const { data: url } = useUserPhoto(user?.azureUniqueId); + if (!user) { + return ( + + + +
+ +
+ + +
+
+
+ ); + } + + return ( + + +
+ {user?.name} +
+ {getJobTitle(user)} + {getDepartment(user)} +
+
+
+ ); +}; diff --git a/client/packages/components/src/components/my-account/components/ProfileContactDetails.tsx b/client/packages/components/src/components/my-account/components/ProfileContactDetails.tsx new file mode 100644 index 000000000..924f0d68a --- /dev/null +++ b/client/packages/components/src/components/my-account/components/ProfileContactDetails.tsx @@ -0,0 +1,67 @@ +import styled from 'styled-components'; +import { Icon, Typography } from '@equinor/eds-core-react'; + +import { ProfileListItem } from './ProfileListItem'; +import { TeamsIcon, DelveIcon, Skeleton } from '@portal/ui'; + +import { PersonDetails } from '@portal/types'; + +const Style = { + InfoWrapper: styled.div` + display: flex; + flex-direction: column; + padding: 1rem, 0rem; + `, +}; + +export const ProfileContactDetails = ({ user, isLoading }: { user?: PersonDetails; isLoading: boolean }) => { + return ( + + Contact Details + {user?.mail ? ( + } + href={`mailto:${user.mail}`} + toCopy={user.mail} + text={user.mail} + title="Email address" + /> + ) : ( + isLoading && + )} + {user?.mobilePhone ? ( + } + href={`callto:${user.mobilePhone}`} + toCopy={user.mobilePhone} + text={user.mobilePhone} + title="Phone number" + /> + ) : ( + isLoading && + )} + {user?.mail ? ( + } + href={`msteams:/l/chat/0/0?users=${user.mail}`} + toCopy={`msteams:/l/chat/0/0?users=${user.mail}`} + text="Start a chat in Teams" + title={`${user.name} Teams`} + /> + ) : ( + isLoading && + )} + {user?.azureUniqueId ? ( + } + href={`https://eur.delve.office.com/?u=${user.azureUniqueId}&v=work`} + toCopy={`https://eur.delve.office.com/?u=${user.azureUniqueId}&v=work`} + text="Go to Delve account" + title={`${user.name} Delve account`} + /> + ) : ( + isLoading && + )} + + ); +}; diff --git a/client/packages/components/src/components/my-account/components/ProfileListItem.tsx b/client/packages/components/src/components/my-account/components/ProfileListItem.tsx new file mode 100644 index 000000000..973e65151 --- /dev/null +++ b/client/packages/components/src/components/my-account/components/ProfileListItem.tsx @@ -0,0 +1,101 @@ +import { Button, Icon, Tooltip } from '@equinor/eds-core-react'; +import { tokens } from '@equinor/eds-tokens'; +import { ReactNode, useState } from 'react'; +import styled from 'styled-components'; + +const Styles = { + Icon: styled.span` + color: ${tokens.colors.text.static_icons__tertiary.hex}; + padding-left: 0.5rem; + `, + Link: styled.a` + cursor: pointer; + display: flex; + color: ${tokens.colors.interactive.primary__resting.hex}; + align-items: center; + :hover { + background-color: ${tokens.colors.interactive.primary__hover_alt.hex}; + } + padding: 0.5rem; + border-radius: 4px; + `, + LinkContent: styled.div` + display: flex; + gap: 1rem; + + width: 100%; + align-items: center; + `, +}; + +type ProfileListItemProps = { + href: string; + title: string; + text: string; + toCopy: string; + icon: ReactNode; +}; +type copyContent = { + success: boolean; + text: string; +}; + +export const ProfileListItem = ({ href, title, text, icon, toCopy }: ProfileListItemProps) => { + const [copySuccess, setCopySuccess] = useState({ + success: false, + text: `Copy ${title}`, + }); + const [showCopy, setShowCopy] = useState(false); + + return ( + { + setShowCopy(true); + }} + onMouseLeave={() => { + setShowCopy(false); + }} + > + + {icon} + {text} + + + + ); +}; diff --git a/client/packages/components/src/components/my-account/components/ProfileManager.tsx b/client/packages/components/src/components/my-account/components/ProfileManager.tsx new file mode 100644 index 000000000..816a8eb50 --- /dev/null +++ b/client/packages/components/src/components/my-account/components/ProfileManager.tsx @@ -0,0 +1,9 @@ +import { useUser } from '@portal/core'; +import { PersonDetails } from '@portal/types'; + +import { ProfileCardHeader } from './ProfileCardHeader'; + +export const ProfileManagerCard = ({ user }: { user?: PersonDetails }) => { + const { data } = useUser(user?.managerAzureUniqueId); + return ; +}; diff --git a/client/packages/components/src/components/my-allocations-tab/MyAllocationTab.tsx b/client/packages/components/src/components/my-allocations-tab/MyAllocationTab.tsx new file mode 100644 index 000000000..b823eb86d --- /dev/null +++ b/client/packages/components/src/components/my-allocations-tab/MyAllocationTab.tsx @@ -0,0 +1,92 @@ +import { Button, Icon, Typography } from '@equinor/eds-core-react'; +import { Style as BaseStyle } from '../my-roles-tab/MyRolesTab'; + +import { arrow_back, tag_relations } from '@equinor/eds-icons'; +import styled from '@emotion/styled'; +import { tokens } from '@equinor/eds-tokens'; +import { PersonPosition } from '@portal/types'; +import { getFusionPortalURL } from '@portal/utils'; + +const Style = { + ...BaseStyle, + PositionWrapper: styled.div` + border: 1px solid #a3a3a3; + border-radius: 4px; + margin-left: 0.5rem; + `, + ProjectButton: styled(Button)` + padding: 0.5rem; + width: 100%; + border-radius: 0; + border-bottom: 1px solid #a3a3a3; + > * { + justify-content: start; + } + + :hover { + border-radius: 0; + border-bottom: 1px solid #a3a3a3; + } + `, + PositionButton: styled(Button)` + padding: 1rem; + display: flex; + align-items: center; + gap: 1rem; + height: auto; + `, + Icon: styled(Icon)` + padding-right: 1rem; + `, +}; + +export const MyAllocationTab = ({ onClick, positions }: { onClick: VoidFunction; positions?: PersonPosition[] }) => { + return ( + + + + + My Allocation + + {positions + ?.filter((item) => item.appliesTo && new Date(item.appliesTo) > new Date()) + ?.map((item) => ( + + + {item.project.name} + + + +
+ + {item.name} + + + <> + {item.appliesFrom && new Date(item.appliesFrom).toLocaleDateString('en-US')} + {' - '} + {item.appliesTo && new Date(item.appliesTo).toLocaleDateString('en-US')} ( + {item.workload}%) + + +
+
+
+ ))} +
+ ); +}; diff --git a/client/packages/components/src/components/my-roles-tab/MyRolesTab.tsx b/client/packages/components/src/components/my-roles-tab/MyRolesTab.tsx new file mode 100644 index 000000000..617b497ff --- /dev/null +++ b/client/packages/components/src/components/my-roles-tab/MyRolesTab.tsx @@ -0,0 +1,87 @@ +import { Button, Icon, Typography } from '@equinor/eds-core-react'; +import styled from 'styled-components'; +import { PersonDetails } from '@portal/types'; +import { Switch } from '@equinor/eds-core-react'; + +import { arrow_back } from '@equinor/eds-icons'; +import { tokens } from '@equinor/eds-tokens'; +import { useUpdateMyRoles } from './hooks/use-update-my-roles-query'; +import { expiresIn } from './utils/expires-in'; + +export const Style = { + Wrapper: styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem, 1rem; + `, + TopWrapper: styled.div` + display: flex; + flex-direction: row; + gap: 1rem; + padding: 1rem, 0; + align-items: center; + `, + Role: styled.div` + display: flex; + flex-direction: row; + padding: 0.5rem; + justify-content: space-between; + border: 1px solid grey; + border-radius: 4px; + margin-left: 0.5rem; + `, + RoleTop: styled.div` + display: flex; + flex-direction: row; + gap: 1rem; + align-items: center; + `, + Indicator: styled.div<{ isActive: boolean }>` + height: 40px; + width: 0.5rem; + background-color: ${({ isActive }) => + isActive ? tokens.colors.interactive.primary__resting.hex : tokens.colors.interactive.disabled__text.hex}; + `, +}; + +export const MyRolesTab = ({ onClick, user }: { onClick: VoidFunction; user?: PersonDetails }) => { + const { roles, mutate } = useUpdateMyRoles(user?.roles); + + return ( + + + + My Roles + + + {roles?.map((role) => ( + + + +
+ {role.displayName} + + {role.name} ({String(role.isActive)}){' '} + {role.errorMessage + ? role.errorMessage + : role.activeToUtc && `- ${expiresIn(role.activeToUtc)}`} + +
+
+ + {role.onDemandSupport && ( + { + mutate({ roleName: role.name, isActive: e.target.checked }); + }} + /> + )} +
+ ))} +
+ ); +}; diff --git a/client/packages/components/src/components/my-roles-tab/hooks/use-update-my-roles-query.ts b/client/packages/components/src/components/my-roles-tab/hooks/use-update-my-roles-query.ts new file mode 100644 index 000000000..47f20de45 --- /dev/null +++ b/client/packages/components/src/components/my-roles-tab/hooks/use-update-my-roles-query.ts @@ -0,0 +1,93 @@ +import { PersonRole, Role } from '@portal/types'; + +import { useFramework } from '@equinor/fusion-framework-react'; + +import { useQuery, useMutation, useQueryClient } from 'react-query'; +import { useCurrentUser } from '@equinor/fusion-framework-react/hooks'; +import { mutateArray } from 'packages/utils/src'; + +import { useMemo } from 'react'; + +type UpdateProps = { roleName: string; isActive: boolean }; + +export const useUpdateUserRoleQuery = (roles?: Role[], userId?: string) => { + const client = useFramework().modules.serviceDiscovery.createClient('people'); + + const queryClient = useQueryClient(); + + const sortedOnDemandSupportRolesTop = useMemo( + () => roles?.sort((a, b) => (a.onDemandSupport === b.onDemandSupport ? 0 : a.onDemandSupport ? -1 : 1)), + [roles] + ); + + const { data } = useQuery({ + queryKey: ['my-roles'], + initialData: sortedOnDemandSupportRolesTop, + queryFn: () => sortedOnDemandSupportRolesTop, + }); + + const { mutate } = useMutation({ + mutationKey: ['my-roles'], + mutationFn: async ({ roleName, isActive }: UpdateProps) => { + const currentMyRoles = queryClient.getQueryData(['my-roles']); + if (!currentMyRoles) return; + + const data = await ( + await client + ).json(`/persons/${userId}/roles/${roleName}`, { + method: 'Patch', + body: JSON.stringify({ isActive }), + }); + + return mutateArray(currentMyRoles, 'name') + .mutate((roles) => { + if (data) { + roles[data.name] = data; + } + }) + .getValue(); + }, + onSuccess(data) { + queryClient.setQueryData(['my-roles'], data); + queryClient.invalidateQueries(['current-user-info']); + }, + onError(error, variables, context) { + if (!context) return; + + const roles = mutateArray(context, 'name') + .mutate((roles) => { + roles[variables.roleName] = { + ...roles[variables.roleName], + activeToUtc: undefined, + errorMessage: (error as Error)?.message, + }; + }) + .getValue(); + + queryClient.setQueriesData(['my-roles'], roles); + }, + onMutate: async ({ roleName, isActive }: UpdateProps) => { + const currentMyRoles = queryClient.getQueryData(['my-roles']); + if (!currentMyRoles) return; + + return mutateArray(currentMyRoles, 'name') + .mutate((roles) => { + roles[roleName].isActive = isActive; + roles[roleName].activeToUtc = undefined; + roles[roleName].errorMessage = undefined; + }) + .getValue(); + }, + }); + + return { roles: data, mutate }; +}; + +export const useUpdateUserRoles = (roles?: Role[], userId?: string) => { + return useUpdateUserRoleQuery(roles, userId); +}; + +export const useUpdateMyRoles = (roles?: Role[]) => { + const user = useCurrentUser(); + return useUpdateUserRoleQuery(roles, user?.localAccountId); +}; diff --git a/client/packages/components/src/components/my-roles-tab/utils/expires-in.test.ts b/client/packages/components/src/components/my-roles-tab/utils/expires-in.test.ts new file mode 100644 index 000000000..1f713c7e0 --- /dev/null +++ b/client/packages/components/src/components/my-roles-tab/utils/expires-in.test.ts @@ -0,0 +1,38 @@ +// Import the expiresIn function +import { expiresIn } from './expires-in'; +import { it, describe, expect } from 'vitest'; + +describe('expiresIn function', () => { + it('should return "Expired" if the target date is in the past', () => { + // Specify a past date + const pastDate = '2023-01-01T00:00:00.000Z'; + + // Call the expiresIn function with the past date + const result = expiresIn(pastDate); + + // Expect the result to be 'Expired' + expect(result).toBe('Expired'); + }); + + it('should return a string indicating the remaining time in hours if the target date is in the future', () => { + // Specify a future date, e.g., 2 hours from now + const futureDate = new Date(Date.now() + 2 * 3600000).toISOString(); + + // Call the expiresIn function with the future date + const result = expiresIn(futureDate); + + // Expect the result to match the expected format, e.g., "Expires in 2 hours" + expect(result).toMatch(/^Expires in \d+ hours$/); + }); + + it('should handle invalid date input and return "Expired"', () => { + // Provide an invalid date format + const invalidDate = 'invalid-date-format'; + + // Call the expiresIn function with the invalid date + const result = expiresIn(invalidDate); + + // Expect the result to be 'Expired' + expect(result).toBe('Expired'); + }); +}); diff --git a/client/packages/components/src/components/my-roles-tab/utils/expires-in.ts b/client/packages/components/src/components/my-roles-tab/utils/expires-in.ts new file mode 100644 index 000000000..084402c32 --- /dev/null +++ b/client/packages/components/src/components/my-roles-tab/utils/expires-in.ts @@ -0,0 +1,18 @@ +/** + * Calculates the time remaining until a specified date and returns a human-readable string. + * + * @param activeTo - A string representing the target expiration date in a valid date format. + * @returns A string indicating the remaining time until the specified date. + */ +export const expiresIn = (activeTo: string) => { + const activeToDate = new Date(activeTo).getTime(); + const now = new Date().getTime(); + + if (isNaN(activeToDate) || now > activeToDate) { + return 'Expired'; + } + + const millisecondsInAnHour = 36e5; + + return `Expires in ${Math.ceil(Math.abs(activeToDate - now) / millisecondsInAnHour)} hours`; +}; diff --git a/client/packages/components/src/components/portal-settings-tab/PortalSettingsTab.tsx b/client/packages/components/src/components/portal-settings-tab/PortalSettingsTab.tsx new file mode 100644 index 000000000..0385291a8 --- /dev/null +++ b/client/packages/components/src/components/portal-settings-tab/PortalSettingsTab.tsx @@ -0,0 +1,20 @@ +import { Button, Icon, Typography } from '@equinor/eds-core-react'; +import { Style } from '../my-roles-tab/MyRolesTab'; +import { PersonDetails } from '@portal/types'; +import { arrow_back } from '@equinor/eds-icons'; +import { InfoMessage } from '@equinor/portal-ui'; + +export const PortalSettingsTab = ({ onClick }: { onClick: VoidFunction; user?: PersonDetails }) => { + return ( + + + + + Portal Settings + + This functionality is not yet implemented. + + ); +}; diff --git a/client/packages/components/src/components/presence-indicator/PresenceIndicator.tsx b/client/packages/components/src/components/presence-indicator/PresenceIndicator.tsx new file mode 100644 index 000000000..075b75588 --- /dev/null +++ b/client/packages/components/src/components/presence-indicator/PresenceIndicator.tsx @@ -0,0 +1,23 @@ +import { useMemo } from 'react'; +import { getPresenceInfo } from './utils/parse-presence-status'; +import { usePresenceQuery } from './hooks/use-presence-query'; +import styled from 'styled-components'; + +const Wrapper = styled.div` + display: flex; + align-items: center; + gap: 1rem; + padding: 0 1rem; +`; + +export const PresenceIndicator = () => { + const { data } = usePresenceQuery(); + const { icon, status } = useMemo(() => getPresenceInfo(data?.availability), [data]); + + return ( + + {icon} + {status} + + ); +}; diff --git a/client/packages/components/src/components/presence-indicator/hooks/use-presence-query.ts b/client/packages/components/src/components/presence-indicator/hooks/use-presence-query.ts new file mode 100644 index 000000000..e211cbfd6 --- /dev/null +++ b/client/packages/components/src/components/presence-indicator/hooks/use-presence-query.ts @@ -0,0 +1,15 @@ +import { useFramework } from '@equinor/fusion-framework-react'; +import { useCurrentUser } from '@equinor/fusion-framework-react/hooks'; +import { useQuery } from 'react-query'; +import { getPresence } from '../query/getPresence'; + +export const usePresenceQuery = () => { + const client = useFramework().modules.serviceDiscovery.createClient('people'); + const currentUser = useCurrentUser(); + + return useQuery({ + queryKey: ['presence'], + queryFn: async () => getPresence(await client, currentUser?.localAccountId ?? ''), + refetchInterval: 5 * 1000 * 60, + }); +}; diff --git a/client/packages/components/src/components/presence-indicator/index.ts b/client/packages/components/src/components/presence-indicator/index.ts new file mode 100644 index 000000000..7074ed6a6 --- /dev/null +++ b/client/packages/components/src/components/presence-indicator/index.ts @@ -0,0 +1 @@ +export * from './PresenceIndicator'; diff --git a/client/packages/components/src/components/presence-indicator/query/getPresence.ts b/client/packages/components/src/components/presence-indicator/query/getPresence.ts new file mode 100644 index 000000000..56d7dd6d2 --- /dev/null +++ b/client/packages/components/src/components/presence-indicator/query/getPresence.ts @@ -0,0 +1,8 @@ +import { IHttpClient } from '@equinor/fusion-framework-module-http'; +import { Presence } from '../types/types'; + +export async function getPresence(client: IHttpClient, userId: string): Promise { + const res = await client.fetch(`/persons/${userId}/presence`); + if (!res.ok) throw res; + return (await res.json()) as Presence; +} diff --git a/client/packages/portal-ui/src/avatar/types.ts b/client/packages/components/src/components/presence-indicator/types/types.ts similarity index 100% rename from client/packages/portal-ui/src/avatar/types.ts rename to client/packages/components/src/components/presence-indicator/types/types.ts diff --git a/client/packages/components/src/components/presence-indicator/utils/parse-presence-status.test.tsx b/client/packages/components/src/components/presence-indicator/utils/parse-presence-status.test.tsx new file mode 100644 index 000000000..1e4ed9915 --- /dev/null +++ b/client/packages/components/src/components/presence-indicator/utils/parse-presence-status.test.tsx @@ -0,0 +1,26 @@ +import { it, describe, expect } from 'vitest'; +import { getPresenceInfo } from './parse-presence-status'; +import { Icon } from '@equinor/eds-core-react'; +import { account_circle, help_outline } from '@equinor/eds-icons'; + +describe('getPresenceInfo', () => { + it('should return the correct PresenceInfo object for each availability status', () => { + // Test for 'Available' status + expect(getPresenceInfo('Available')).toEqual({ + icon: , + status: 'Available', + }); + + // Add similar expect statements for other availability statuses like 'Away', 'BeRightBack', 'Busy', 'DoNotDisturb', 'Offline', and test that they return the correct PresenceInfo objects. + }); + + it('should return the correct default PresenceInfo object for an unknown status', () => { + // Test for an unknown status (status is undefined) + expect(getPresenceInfo(undefined)).toEqual({ + icon: , + status: 'Unknown', + }); + + // You can also add more test cases for other unknown status scenarios if needed. + }); +}); diff --git a/client/packages/components/src/components/presence-indicator/utils/parse-presence-status.tsx b/client/packages/components/src/components/presence-indicator/utils/parse-presence-status.tsx new file mode 100644 index 000000000..07a586e91 --- /dev/null +++ b/client/packages/components/src/components/presence-indicator/utils/parse-presence-status.tsx @@ -0,0 +1,54 @@ +import { Icon } from '@equinor/eds-core-react'; + +import { Availability, PresenceInfo } from '../types/types'; +import { account_circle, close_circle_outlined, help_outline } from '@equinor/eds-icons'; + +export function getPresenceInfo(status: Availability | undefined): PresenceInfo { + if (!status) return { icon: , status: 'Unknown' }; + + switch (status) { + case 'Available': + return { + icon: , + status: 'Available', + }; + + case 'Away': + return { + icon: , + status: 'Away', + }; + + case 'BeRightBack': + return { + icon: , + status: 'Be right back', + }; + + case 'Busy': + return { + icon: , + status: 'Busy', + }; + + case 'DoNotDisturb': { + return { + icon: , + status: 'Do not disturb', + }; + } + + case 'Offline': + return { + icon: , + status: 'Offline', + }; + + default: { + return { + icon: , + status: 'Unknown', + }; + } + } +} diff --git a/client/packages/components/src/components/top-bar-avatar/TopBarAvatar.tsx b/client/packages/components/src/components/top-bar-avatar/TopBarAvatar.tsx new file mode 100644 index 000000000..5de8c812f --- /dev/null +++ b/client/packages/components/src/components/top-bar-avatar/TopBarAvatar.tsx @@ -0,0 +1,8 @@ +import { useCurrentUser, useCurrentUserPhoto } from '@portal/core'; +import { Avatar, getAccountTypeColor } from '@portal/ui'; + +export const TopBarAvatar = (): JSX.Element | null => { + const { data: url } = useCurrentUserPhoto(); + const currentUser = useCurrentUser(); + return ; +}; diff --git a/client/packages/components/src/index.ts b/client/packages/components/src/index.ts new file mode 100644 index 000000000..17ed17f3e --- /dev/null +++ b/client/packages/components/src/index.ts @@ -0,0 +1,4 @@ +// Export Portal Components +export * from './components/top-bar-avatar/TopBarAvatar'; +export * from './components/presence-indicator'; +export * from './components/my-account/MyAccount'; diff --git a/client/packages/components/tsconfig.json b/client/packages/components/tsconfig.json new file mode 100644 index 000000000..601ec8cb8 --- /dev/null +++ b/client/packages/components/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "noFallthroughCasesInSwitch": true, + "outDir": "../../dist/out-tsc", + "types": ["node"], + + }, + "files": [], + + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.d.ts", "src/mock"] +} diff --git a/client/packages/components/vite.config.ts b/client/packages/components/vite.config.ts new file mode 100644 index 000000000..9874be8eb --- /dev/null +++ b/client/packages/components/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + }, +}); diff --git a/client/packages/core/README.md b/client/packages/core/README.md new file mode 100644 index 000000000..216a6df6c --- /dev/null +++ b/client/packages/core/README.md @@ -0,0 +1,37 @@ + + +
+
+ + + + +

Client - Project Core

+ +

+ This projects is just for core functionality. +
+ Explore Project Portal the docs +
+ Web Client Run-book +

+
+ + +## Project UI + +> [!NOTE] +> The project can be use like this, import component form `@portal/core` + +The is project should contain only core functionality and supporting components. +Imports from `@portal/types` are allowed but other imports should be dismissed in pull-requests. + +[![typescript][typescript]][typescript-url] +[![React][react.js]][react-url] + + +[react.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB +[react-url]: https://reactjs.org/ + +[typescript]: https://img.shields.io/badge/typescript-20232A?style=for-the-badge&logo=typescript&logoColor=61DAFB +[typescript-url]: https://typescriptlang.org diff --git a/client/packages/core/package.json b/client/packages/core/package.json new file mode 100644 index 000000000..3618026ab --- /dev/null +++ b/client/packages/core/package.json @@ -0,0 +1,8 @@ +{ + "name": "@portal/core", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "build": "tsc" + } +} \ No newline at end of file diff --git a/client/packages/core/src/index.ts b/client/packages/core/src/index.ts new file mode 100644 index 000000000..e5abc8565 --- /dev/null +++ b/client/packages/core/src/index.ts @@ -0,0 +1 @@ +export * from './user'; diff --git a/client/packages/core/src/user/hooks/index.ts b/client/packages/core/src/user/hooks/index.ts new file mode 100644 index 000000000..1ab83d37c --- /dev/null +++ b/client/packages/core/src/user/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useUserInfo'; +export * from './userPhoto'; diff --git a/client/packages/core/src/user/hooks/useUserInfo.ts b/client/packages/core/src/user/hooks/useUserInfo.ts new file mode 100644 index 000000000..727136950 --- /dev/null +++ b/client/packages/core/src/user/hooks/useUserInfo.ts @@ -0,0 +1,9 @@ +import * as userQueries from './useUserQueries'; + +export const useCurrentUser = () => { + return userQueries.useCurrentUserQuery(); +}; + +export const useUser = (userId?: string) => { + return userQueries.useUserQuery(userId); +}; diff --git a/client/packages/core/src/user/hooks/useUserQueries.ts b/client/packages/core/src/user/hooks/useUserQueries.ts new file mode 100644 index 000000000..fff4920f4 --- /dev/null +++ b/client/packages/core/src/user/hooks/useUserQueries.ts @@ -0,0 +1,38 @@ +import { useFramework } from '@equinor/fusion-framework-react'; +import { useCurrentUser } from '@equinor/fusion-framework-react/hooks'; + +import { useQuery } from 'react-query'; + +import { getCurrentUserInfo, getUserInfo, getUserPhotoById } from '../queries'; +import { PersonDetails, FusionError } from '@portal/types'; + +export const useCurrentUserQuery = () => { + const client = useFramework().modules.serviceDiscovery.createClient('people'); + const user = useCurrentUser(); + + return useQuery({ + queryKey: ['current-user-info', user?.localAccountId], + queryFn: async () => getCurrentUserInfo(await client, user?.localAccountId), + enabled: Boolean(user?.localAccountId), + }); +}; + +export const useUserQuery = (azureId?: string) => { + const client = useFramework().modules.serviceDiscovery.createClient('people'); + return useQuery({ + queryKey: ['user-info', azureId], + queryFn: async () => getUserInfo(await client, azureId), + enabled: Boolean(azureId), + }); +}; + +export const useUserPhotoQuery = (userId?: string) => { + const client = useFramework().modules.serviceDiscovery.createClient('people'); + + return useQuery({ + queryKey: ['user-photo', userId], + queryFn: async () => getUserPhotoById(await client, userId ?? ''), + enabled: Boolean(userId), + _defaulted: undefined, + }); +}; diff --git a/client/packages/core/src/user/hooks/userPhoto.ts b/client/packages/core/src/user/hooks/userPhoto.ts new file mode 100644 index 000000000..741c59744 --- /dev/null +++ b/client/packages/core/src/user/hooks/userPhoto.ts @@ -0,0 +1,11 @@ +import { useCurrentUser } from '@equinor/fusion-framework-react/hooks'; +import * as userQueries from './useUserQueries'; + +export const useUserPhoto = (userId?: string) => { + return userQueries.useUserPhotoQuery(userId); +}; + +export const useCurrentUserPhoto = () => { + const user = useCurrentUser(); + return userQueries.useUserPhotoQuery(user?.localAccountId); +}; diff --git a/client/packages/core/src/user/index.ts b/client/packages/core/src/user/index.ts new file mode 100644 index 000000000..fd70c4250 --- /dev/null +++ b/client/packages/core/src/user/index.ts @@ -0,0 +1,2 @@ +export * from './hooks'; +export * from './utils'; diff --git a/client/packages/core/src/user/queries/index.ts b/client/packages/core/src/user/queries/index.ts new file mode 100644 index 000000000..1bc0826bf --- /dev/null +++ b/client/packages/core/src/user/queries/index.ts @@ -0,0 +1,2 @@ +export * from './user-info-query'; +export * from './user-photo-query'; diff --git a/client/packages/core/src/user/queries/user-info-query.ts b/client/packages/core/src/user/queries/user-info-query.ts new file mode 100644 index 000000000..70115ef7a --- /dev/null +++ b/client/packages/core/src/user/queries/user-info-query.ts @@ -0,0 +1,14 @@ +import { IHttpClient } from '@equinor/fusion-framework-module-http'; +import { PersonDetails } from '@portal/types'; + +export async function getUserInfo(client: IHttpClient, azureUserId?: string): Promise { + const res = await client.fetch(`/persons/${azureUserId}?api-version=3.0`); + if (!res.ok) throw res; + return res.json(); +} + +export async function getCurrentUserInfo(client: IHttpClient, azureUserId?: string): Promise { + const res = await client.fetch(`/persons/${azureUserId}?api-version=3.0&$expand=positions,contracts,roles`); + if (!res.ok) throw res; + return res.json(); +} diff --git a/client/packages/core/src/user/queries/user-photo-query.ts b/client/packages/core/src/user/queries/user-photo-query.ts new file mode 100644 index 000000000..71b9633e8 --- /dev/null +++ b/client/packages/core/src/user/queries/user-photo-query.ts @@ -0,0 +1,7 @@ +import { IHttpClient } from '@equinor/fusion-framework-module-http'; + +export async function getUserPhotoById(client: IHttpClient, userId: string): Promise { + const res = await client.fetch(`/persons/${userId}/photo?api-version=2.0`); + if (!res.ok) throw res; + return URL.createObjectURL(await res.blob()); +} diff --git a/client/packages/core/src/user/utils/index.ts b/client/packages/core/src/user/utils/index.ts new file mode 100644 index 000000000..65f7249bd --- /dev/null +++ b/client/packages/core/src/user/utils/index.ts @@ -0,0 +1 @@ +export * from './personCardUtils'; diff --git a/client/packages/core/src/user/utils/personCardUtils.ts b/client/packages/core/src/user/utils/personCardUtils.ts new file mode 100644 index 000000000..0dee74f75 --- /dev/null +++ b/client/packages/core/src/user/utils/personCardUtils.ts @@ -0,0 +1,30 @@ +import { PersonDetails } from '@portal/types'; +/** + * @returns if the person does not have a job title and if the persons account type is also "External", job title becomes "External" + */ +export const getJobTitle = (person: PersonDetails) => { + if (!person.jobTitle) { + return person.accountClassification === 'External' ? 'External' : null; + } + return person.jobTitle; +}; + +/** + * @returns if the person does not have a department and if that persons account type is "External", department becomes domain name from the email address + */ +export const getDepartment = (person: PersonDetails) => { + const domain = person.mail?.split('@')[1].toLowerCase(); + if (!person.department) { + return person.accountClassification === 'External' ? domain : null; + } + return person.department; +}; + +/** Returns intials from first and if available, last name */ +export const getInitials = (name: string) => { + if (name.split(' ').length > 1) { + return name.split(' ')[0][0] + name.split(' ')[1][0]; + } else { + return name.split(' ')[0][0]; + } +}; diff --git a/client/packages/core/tsconfig.json b/client/packages/core/tsconfig.json new file mode 100644 index 000000000..2336d7a34 --- /dev/null +++ b/client/packages/core/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "noFallthroughCasesInSwitch": true, + "outDir": "../../dist/out-tsc", + }, + "files": [], + "include": ["**/*.tsx"] + +} + diff --git a/client/packages/core/vite.config.ts b/client/packages/core/vite.config.ts new file mode 100644 index 000000000..aee81d389 --- /dev/null +++ b/client/packages/core/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import tsconfig from 'vite-tsconfig-paths'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tsconfig()], + test: { + environment: 'jsdom', + globals: true, + server: { + deps: { + fallbackCJS: true, + }, + }, + }, + resolve: { + alias: { + '@equinor/portal-core': path.resolve(__dirname, '../portal-core/src/index.ts'), + '@equinor/portal-ui': path.resolve(__dirname, '../portal-ui/src/index.ts'), + '@portal/utils': path.resolve(__dirname, '../portal-utils/src/index.ts'), + 'portal-types': path.resolve(__dirname, '../types/src/index.ts'), + }, + }, +}); diff --git a/client/packages/portal-client/src/assets/fallback-photo.svg.ts b/client/packages/portal-client/src/assets/fallback-photo.svg.ts new file mode 100644 index 000000000..f5f760e17 --- /dev/null +++ b/client/packages/portal-client/src/assets/fallback-photo.svg.ts @@ -0,0 +1,2 @@ +export const svg = ``; +export default svg; diff --git a/client/packages/portal-client/src/components/portal-menu/PortalMenu.tsx b/client/packages/portal-client/src/components/portal-menu/PortalMenu.tsx index 26f2c4c84..5c3aca4c9 100644 --- a/client/packages/portal-client/src/components/portal-menu/PortalMenu.tsx +++ b/client/packages/portal-client/src/components/portal-menu/PortalMenu.tsx @@ -1,7 +1,7 @@ import { Search } from '@equinor/eds-core-react'; import { useAppGroupsQuery, appsMatchingSearch } from '@equinor/portal-core'; import { GroupWrapper, InfoMessage, LoadingMenu, PortalMenu, StyledCategoryItem } from '@equinor/portal-ui'; -import { useObservable, customAppGroupArraySort, getDisabledApps, getPinnedAppsGroup } from '@equinor/portal-utils'; +import { useObservable, customAppGroupArraySort, getDisabledApps, getPinnedAppsGroup } from '@portal/utils'; import { combineLatest, map } from 'rxjs'; import { menuFavoritesController, useAppModule, useMenuContext } from '@equinor/portal-core'; diff --git a/client/packages/portal-client/vite.config.ts b/client/packages/portal-client/vite.config.ts index 27d755fd0..26f6d4955 100644 --- a/client/packages/portal-client/vite.config.ts +++ b/client/packages/portal-client/vite.config.ts @@ -54,9 +54,13 @@ export default defineConfig(({ mode }) => { '@equinor/portal-core': path.resolve(__dirname, '../portal-core/src/index.ts'), '@equinor/portal-pages': path.resolve(__dirname, '../portal-pages/src/index.ts'), '@equinor/portal-ui': path.resolve(__dirname, '../portal-ui/src/index.ts'), - '@equinor/portal-utils': path.resolve(__dirname, '../portal-utils/src/index.ts'), + '@portal/utils': path.resolve(__dirname, '../utils/src/index.ts'), '@equinor/service-message': path.resolve(__dirname, '../service-message/index.ts'), '@equinor/notification': path.resolve(__dirname, '../notification/index.ts'), + '@portal/types': path.resolve(__dirname, '../types/src/index.ts'), + '@portal/core': path.resolve(__dirname, '../core/src/index.ts'), + '@portal/ui': path.resolve(__dirname, '../ui/src/index.ts'), + '@portal/components': path.resolve(__dirname, '../components/src/index.ts'), }, }, }; diff --git a/client/packages/portal-core/src/app/utils/app-get-legacy-client-config.test.ts b/client/packages/portal-core/src/app/utils/app-get-legacy-client-config.test.ts index 8c0dca0bf..bba628ada 100644 --- a/client/packages/portal-core/src/app/utils/app-get-legacy-client-config.test.ts +++ b/client/packages/portal-core/src/app/utils/app-get-legacy-client-config.test.ts @@ -27,6 +27,6 @@ describe('getLegacyClientConfig', () => { describe('getFusionLegacyEnvIdentifier', () => { test('should return env identifier', () => { const env = getFusionLegacyEnvIdentifier(); - expect(env).toBe('ci'); + expect(env).toBe('CI'); }); }); diff --git a/client/packages/portal-core/src/framework-configurator/portal-context-configurators.ts b/client/packages/portal-core/src/framework-configurator/portal-context-configurators.ts index 63b27f532..8a8a1169f 100644 --- a/client/packages/portal-core/src/framework-configurator/portal-context-configurators.ts +++ b/client/packages/portal-core/src/framework-configurator/portal-context-configurators.ts @@ -1,5 +1,5 @@ import { IContextProvider } from '@equinor/fusion-framework-module-context'; -import { storage } from '@equinor/portal-utils'; +import { storage } from '@portal/utils'; import { getContextFormUrl } from '../utils'; import { setContextHistory } from './portal-context-history'; diff --git a/client/packages/portal-core/src/framework-configurator/portal-context-history.ts b/client/packages/portal-core/src/framework-configurator/portal-context-history.ts index 98c684040..72ba1b9b2 100644 --- a/client/packages/portal-core/src/framework-configurator/portal-context-history.ts +++ b/client/packages/portal-core/src/framework-configurator/portal-context-history.ts @@ -1,6 +1,6 @@ import { ContextItem } from '@equinor/fusion-framework-module-context'; import { ContextResult } from '@equinor/fusion-react-context-selector'; -import { moveItemToTopByIndex, storage } from '@equinor/portal-utils'; +import { moveItemToTopByIndex, storage } from '@portal/utils'; const CONTEXT_HISTORY_SOCAGE_KEY = 'contextHistory'; const CONTEXT_HISTORY_LENGTH_KEY = 'contextHistoryLength'; diff --git a/client/packages/portal-core/src/menu/menuFavorites.ts b/client/packages/portal-core/src/menu/menuFavorites.ts index 1b4ca36c0..436d1566f 100644 --- a/client/packages/portal-core/src/menu/menuFavorites.ts +++ b/client/packages/portal-core/src/menu/menuFavorites.ts @@ -1,20 +1,13 @@ -import { createObservableStorage } from '@equinor/portal-utils'; +import { createObservableStorage } from '@portal/utils'; //Key the value is stored under const storageKey = 'menu-favorites'; -const { next, subject$, obs$ } = createObservableStorage( - storageKey, - [] -); +const { next, subject$, obs$ } = createObservableStorage(storageKey, []); export const menuFavoritesController = { - onClickFavorite: (value: string) => - next( - subject$.value.includes(value) - ? subject$.value.filter((s) => s !== value) - : [...subject$.value, value] - ), - //Store everytime a new value is emitted - favorites$: obs$, + onClickFavorite: (value: string) => + next(subject$.value.includes(value) ? subject$.value.filter((s) => s !== value) : [...subject$.value, value]), + //Store everytime a new value is emitted + favorites$: obs$, }; diff --git a/client/packages/portal-core/src/portal-actions/portal-actions-config.tsx b/client/packages/portal-core/src/portal-actions/portal-actions-config.tsx index 3a1c8e560..2722d2be5 100644 --- a/client/packages/portal-core/src/portal-actions/portal-actions-config.tsx +++ b/client/packages/portal-core/src/portal-actions/portal-actions-config.tsx @@ -1,5 +1,5 @@ -import { Bookmarks, FullscreenIcon, MyAccount, Notification, Task } from '@equinor/portal-ui'; - +import { Bookmarks, FullscreenIcon, Notification, Task } from '@equinor/portal-ui'; +import { MyAccount } from '@portal/components'; import { ServiceMessageIcon, ServiceMessages, ServiceMessageTooltip } from '@equinor/service-message'; import { NotificationBell } from '@equinor/notification'; import { PortalAction } from './types'; diff --git a/client/packages/portal-core/src/portal-actions/portal-top-bar-actions.ts b/client/packages/portal-core/src/portal-actions/portal-top-bar-actions.ts index 4304b63e1..a58391a7d 100644 --- a/client/packages/portal-core/src/portal-actions/portal-top-bar-actions.ts +++ b/client/packages/portal-core/src/portal-actions/portal-top-bar-actions.ts @@ -1,4 +1,4 @@ -import { createObservableStorage } from '@equinor/portal-utils'; +import { createObservableStorage } from '@portal/utils'; import { combineLatestWith, map } from 'rxjs'; import { portalActions } from './portal-actions'; import { actions } from './portal-actions-config'; @@ -6,15 +6,13 @@ import { PortalTopBarActions } from './types'; const TOP_BAR_ACTIONS_STORAGE_KEY = 'topBarActions'; export const topBarActionsIds$ = createObservableStorage( - TOP_BAR_ACTIONS_STORAGE_KEY, - actions.map((action) => action.actionId) || [] + TOP_BAR_ACTIONS_STORAGE_KEY, + actions.map((action) => action.actionId) || [] ); export const topBarActions$ = portalActions.actions$.pipe( - combineLatestWith(topBarActionsIds$.obs$), - map(([actions, ids]) => - actions.filter((action) => ids.includes(action.actionId)) - ) + combineLatestWith(topBarActionsIds$.obs$), + map(([actions, ids]) => actions.filter((action) => ids.includes(action.actionId))) ); /** @@ -23,31 +21,26 @@ export const topBarActions$ = portalActions.actions$.pipe( * `filter((action) => action.topParOnly)` will alow items specified to always show in top bar, to be unchanged. */ function toggleTopBarAllActions() { - topBarActionsIds$.next( - topBarActionsIds$.subject$.value.length < - portalActions.actions$.value.length - ? portalActions.actions$.value.map((action) => action.actionId) - : portalActions.actions$.value - .filter((action) => action.topParOnly) - .map((action) => action.actionId) || [] - ); + topBarActionsIds$.next( + topBarActionsIds$.subject$.value.length < portalActions.actions$.value.length + ? portalActions.actions$.value.map((action) => action.actionId) + : portalActions.actions$.value.filter((action) => action.topParOnly).map((action) => action.actionId) || [] + ); } function toggleActionById(actionId: string) { - const topBarActionsIds = topBarActionsIds$.subject$.value; - topBarActionsIds$.next( - topBarActionsIds.includes(actionId) - ? topBarActionsIds.filter( - (topBarActionsId) => topBarActionsId !== actionId - ) - : [...topBarActionsIds, actionId] - ); + const topBarActionsIds = topBarActionsIds$.subject$.value; + topBarActionsIds$.next( + topBarActionsIds.includes(actionId) + ? topBarActionsIds.filter((topBarActionsId) => topBarActionsId !== actionId) + : [...topBarActionsIds, actionId] + ); } export const portalTopBarActions: PortalTopBarActions = { - topBarActionsIds$, - topBarActions$, - setActiveActionById: portalActions.setActiveActionById, - toggleTopBarAllActions, - toggleActionById, + topBarActionsIds$, + topBarActions$, + setActiveActionById: portalActions.setActiveActionById, + toggleTopBarAllActions, + toggleActionById, }; diff --git a/client/packages/portal-core/src/providers/current-view/CurrentViewContext.tsx b/client/packages/portal-core/src/providers/current-view/CurrentViewContext.tsx index a621a644f..aae181c8e 100644 --- a/client/packages/portal-core/src/providers/current-view/CurrentViewContext.tsx +++ b/client/packages/portal-core/src/providers/current-view/CurrentViewContext.tsx @@ -1,4 +1,4 @@ -import { useObservable } from '@equinor/portal-utils'; +import { useObservable } from 'packages/utils/src'; import { ReactNode, useContext, useEffect, useMemo } from 'react'; import { BehaviorSubject } from 'rxjs'; import { useStoreCurrentViewId } from '../../hooks'; diff --git a/client/packages/portal-core/src/providers/current-view/FailedToLoadViews.tsx b/client/packages/portal-core/src/providers/current-view/FailedToLoadViews.tsx index 102593116..4ff5dbd3c 100644 --- a/client/packages/portal-core/src/providers/current-view/FailedToLoadViews.tsx +++ b/client/packages/portal-core/src/providers/current-view/FailedToLoadViews.tsx @@ -1,6 +1,6 @@ import { Button } from '@equinor/eds-core-react'; import { FatalError } from '@equinor/portal-ui'; -import { responseErrorParser } from '@equinor/portal-utils'; +import { responseErrorParser } from '@portal/utils'; type FailedToLoadViewsProps = { error: Response; diff --git a/client/packages/portal-core/src/providers/menu/MenuProvider.tsx b/client/packages/portal-core/src/providers/menu/MenuProvider.tsx index 3b3757172..13843564d 100644 --- a/client/packages/portal-core/src/providers/menu/MenuProvider.tsx +++ b/client/packages/portal-core/src/providers/menu/MenuProvider.tsx @@ -1,4 +1,4 @@ -import { storage } from '@equinor/portal-utils'; +import { storage } from '@portal/utils'; import { createContext, PropsWithChildren, useState } from 'react'; import { useAppGroupsQuery } from '../../queries'; import { IMenuContext, IMenuState } from './menu-types'; diff --git a/client/packages/portal-core/src/queries/fusion/getPresence.ts b/client/packages/portal-core/src/queries/fusion/getPresence.ts deleted file mode 100644 index 998fa933a..000000000 --- a/client/packages/portal-core/src/queries/fusion/getPresence.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IHttpClient } from '@equinor/fusion-framework-module-http'; -import { Presence } from '@equinor/portal-ui'; - -export async function getPresence( - client: IHttpClient, - userId: string -): Promise { - const res = await client.fetch(`/persons/${userId}/presence`); - if (!res.ok) throw res; - return (await res.json()) as Presence; -} diff --git a/client/packages/portal-core/src/queries/fusion/index.ts b/client/packages/portal-core/src/queries/fusion/index.ts index abc7e9d66..ecf401e7b 100644 --- a/client/packages/portal-core/src/queries/fusion/index.ts +++ b/client/packages/portal-core/src/queries/fusion/index.ts @@ -1 +1 @@ -export * from './getPresence'; +export * from '../../../../components/src/components/presence-indicator/query/getPresence'; diff --git a/client/packages/portal-core/src/queries/hooks/index.ts b/client/packages/portal-core/src/queries/hooks/index.ts index 019451c26..ba8133c63 100644 --- a/client/packages/portal-core/src/queries/hooks/index.ts +++ b/client/packages/portal-core/src/queries/hooks/index.ts @@ -1,5 +1,5 @@ export * from './use-app-groups-query'; export * from './use-current-view-query'; -export * from './use-presence-query'; +export * from '../../../../components/src/components/presence-indicator/hooks/use-presence-query'; export * from './use-views-query'; export * from './use-onboarded-context-query'; diff --git a/client/packages/portal-core/src/queries/hooks/use-presence-query.ts b/client/packages/portal-core/src/queries/hooks/use-presence-query.ts deleted file mode 100644 index f307ddece..000000000 --- a/client/packages/portal-core/src/queries/hooks/use-presence-query.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useFramework } from '@equinor/fusion-framework-react'; -import { - useCurrentUser, -} from '@equinor/fusion-framework-react/hooks'; -import { useQuery } from 'react-query'; -import { getPresence } from '../fusion/getPresence'; - -export const usePresenceQuery = () => { - const client = useFramework().modules.serviceDiscovery.createClient('people'); - const currentUser = useCurrentUser(); - - return useQuery({ - queryKey: ['presence'], - queryFn: async () => - getPresence(await client, currentUser?.localAccountId ?? ''), - - refetchInterval: 5 * 1000 * 60, - }); -}; diff --git a/client/packages/portal-core/src/store/view/view-store.ts b/client/packages/portal-core/src/store/view/view-store.ts index a085de308..cbabd38da 100644 --- a/client/packages/portal-core/src/store/view/view-store.ts +++ b/client/packages/portal-core/src/store/view/view-store.ts @@ -1,7 +1,7 @@ -import { storage } from '@equinor/portal-utils'; +import { storage } from '@portal/utils'; export const viewStorage = { - key: 'currentViewId', - readId: (): string | undefined => storage.getItem(viewStorage.key), - storeId: (id: string | undefined) => storage.setItem(viewStorage.key, id), + key: 'currentViewId', + readId: (): string | undefined => storage.getItem(viewStorage.key), + storeId: (id: string | undefined) => storage.setItem(viewStorage.key, id), }; diff --git a/client/packages/portal-core/vite.config.ts b/client/packages/portal-core/vite.config.ts index eba70c750..0eeabf822 100644 --- a/client/packages/portal-core/vite.config.ts +++ b/client/packages/portal-core/vite.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ alias: { '@equinor/portal-core': path.resolve(__dirname, '../portal-core/src/index.ts'), '@equinor/portal-ui': path.resolve(__dirname, '../portal-ui/src/index.ts'), - '@equinor/portal-utils': path.resolve(__dirname, '../portal-utils/src/index.ts'), + '@portal/utils': path.resolve(__dirname, '../portal-utils/src/index.ts'), }, }, }); diff --git a/client/packages/portal-pages/src/pages/project-page/hooks/use-favorites.ts b/client/packages/portal-pages/src/pages/project-page/hooks/use-favorites.ts index 384fbe6f7..c526707f1 100644 --- a/client/packages/portal-pages/src/pages/project-page/hooks/use-favorites.ts +++ b/client/packages/portal-pages/src/pages/project-page/hooks/use-favorites.ts @@ -1,4 +1,4 @@ -import { getDisabledApps, useObservable } from '@equinor/portal-utils'; +import { getDisabledApps, useObservable } from '@portal/utils'; import { menuFavoritesController, useAppGroupsQuery, useAppModule } from '@equinor/portal-core'; import { combineLatest, map } from 'rxjs'; import { useMemo } from 'react'; diff --git a/client/packages/portal-ui/src/avatar/TopBarAvatar.tsx b/client/packages/portal-ui/src/avatar/TopBarAvatar.tsx deleted file mode 100644 index 3b70a26e3..000000000 --- a/client/packages/portal-ui/src/avatar/TopBarAvatar.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Icon } from '@equinor/eds-core-react'; -import { tokens } from '@equinor/eds-tokens'; -import { usePresenceQuery } from '@equinor/portal-core'; - -import { getPresenceInfo } from './parsePresenceStatus'; -import { StyledStatusIconOverAvatar } from './top-bar-avatar.styles'; - -export const TopBarAvatar = (): JSX.Element | null => { - const { data, isLoading, error } = usePresenceQuery(); - - if (isLoading) { - return ( - - ); - } - - if (error || !data) - {return ( - - );} - - const presenceInfo = getPresenceInfo(data.availability); - return ( -
- - - - {presenceInfo.icon} - -
- ); -}; diff --git a/client/packages/portal-ui/src/avatar/index.ts b/client/packages/portal-ui/src/avatar/index.ts deleted file mode 100644 index 06d2f7ce1..000000000 --- a/client/packages/portal-ui/src/avatar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './TopBarAvatar'; -export * from './types'; diff --git a/client/packages/portal-ui/src/avatar/parsePresenceStatus.tsx b/client/packages/portal-ui/src/avatar/parsePresenceStatus.tsx deleted file mode 100644 index b8fc72c57..000000000 --- a/client/packages/portal-ui/src/avatar/parsePresenceStatus.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Icon } from '@equinor/eds-core-react'; -import styled from 'styled-components'; -import { Availability, PresenceInfo } from './types'; - -export function getPresenceInfo( - status: Availability | undefined -): PresenceInfo { - if (!status) return { icon: , status: 'Unknown' }; - - switch (status) { - case 'Available': - return { - icon: , - status: 'Available', - }; - - case 'Away': - return { - icon: , - status: 'Away', - }; - - case 'BeRightBack': - return { - icon: , - status: 'Be right back', - }; - - case 'Busy': - return { - icon: , - status: 'Busy', - }; - - case 'DoNotDisturb': { - return { - icon: , - status: 'Do not disturb', - }; - } - - case 'Offline': - return { - icon: , - status: 'Offline', - }; - - default: { - return { - icon: , - status: 'Unknown', - }; - } - } -} - -const StatusCircle = styled.div<{ color: string }>` - width: 9px; - height: 9px; - border-radius: 50%; - background-color: ${({ color }) => color}; -`; diff --git a/client/packages/portal-ui/src/avatar/presence-observable.ts b/client/packages/portal-ui/src/avatar/presence-observable.ts deleted file mode 100644 index 08dc0f245..000000000 --- a/client/packages/portal-ui/src/avatar/presence-observable.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Observable, interval } from 'rxjs'; -import { startWith, switchMap } from 'rxjs/operators'; -import { Presence } from './types'; -import { IHttpClient } from '@equinor/fusion-framework-module-http'; - -export const getPresence$: (userId: string, client: Promise) => Observable = ( - userId: string, - client: Promise -) => - interval(5000 * 60).pipe( - startWith(0), - switchMap(() => client), - switchMap((s) => s.fetch$(`/persons/${userId}/presence`)), - switchMap((s) => s.json()) - ); diff --git a/client/packages/portal-ui/src/avatar/top-bar-avatar.styles.ts b/client/packages/portal-ui/src/avatar/top-bar-avatar.styles.ts deleted file mode 100644 index cebf2880c..000000000 --- a/client/packages/portal-ui/src/avatar/top-bar-avatar.styles.ts +++ /dev/null @@ -1,31 +0,0 @@ -import styled from 'styled-components'; - -export const StyledStatusIconOverAvatar = styled.div` - position: absolute; - bottom: 0; - right: 0; - width: 10px; - height: 10px; -`; - -export const StyledPresence = styled.div` - display: flex; - align-items: center; - gap: 0.35em; -`; - -export const StyledInfoText = styled.div` - font-size: 12px; - font-weight: 500; -`; - -export const StyledUserName = styled.div` - font-size: 16px; - font-weight: 500; -`; - -export const StyledWrapper = styled.div` - display: flex; - flex-direction: column; - gap: 1em; -`; diff --git a/client/packages/portal-ui/src/index.ts b/client/packages/portal-ui/src/index.ts index 7953c3694..ac0e6ffeb 100644 --- a/client/packages/portal-ui/src/index.ts +++ b/client/packages/portal-ui/src/index.ts @@ -1,11 +1,9 @@ -export * from './avatar'; export * from './bookmarks/Bookmarks'; export * from './divider/Divider'; export * from './fatal-error'; export * from './full-page-loading'; export * from './fullscreen-icon/FullscreenIcon'; export * from './help/Help'; -export * from './my-account/MyAccount'; export * from './widgets'; export * from './header/Icon'; export * from './header/Header'; diff --git a/client/packages/portal-ui/src/my-account/MyAccount.tsx b/client/packages/portal-ui/src/my-account/MyAccount.tsx deleted file mode 100644 index be9ca25d8..000000000 --- a/client/packages/portal-ui/src/my-account/MyAccount.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useCurrentUser } from '@equinor/fusion-framework-react/hooks'; -import { InfoMessage } from '../info-message/InfoMessage'; - -import { PortalActionProps } from '@equinor/portal-core'; - -import { SideSheet } from '@equinor/fusion-react-side-sheet'; - -export function MyAccount({ action, onClose, open }: PortalActionProps) { - const user = useCurrentUser(); - - return ( - - - - - - This functionality is not yet implemented. - - - ); -} diff --git a/client/packages/portal-ui/src/portal-menu/app-card/AppCard.tsx b/client/packages/portal-ui/src/portal-menu/app-card/AppCard.tsx index 16ff171a4..c9dcf855b 100644 --- a/client/packages/portal-ui/src/portal-menu/app-card/AppCard.tsx +++ b/client/packages/portal-ui/src/portal-menu/app-card/AppCard.tsx @@ -1,7 +1,7 @@ import { Icon } from '@equinor/eds-core-react'; import { tokens } from '@equinor/eds-tokens'; import { menuFavoritesController, useMenuContext, useTelemetry } from '@equinor/portal-core'; -import { useObservable } from '@equinor/portal-utils'; +import { useObservable } from '@portal/utils'; import { Link } from 'react-router-dom'; import { map } from 'rxjs'; import styled from 'styled-components'; diff --git a/client/packages/portal-ui/src/portal-menu/group-wrapper/GroupWrapper.tsx b/client/packages/portal-ui/src/portal-menu/group-wrapper/GroupWrapper.tsx index 45e9348be..7ab76b990 100644 --- a/client/packages/portal-ui/src/portal-menu/group-wrapper/GroupWrapper.tsx +++ b/client/packages/portal-ui/src/portal-menu/group-wrapper/GroupWrapper.tsx @@ -2,7 +2,7 @@ import { AppGroup } from '@equinor/portal-core'; import { InfoMessage } from '../../info-message/InfoMessage'; import { Group } from '../group/Group'; import { styles } from '../styles'; -import { getColumnCount } from '@equinor/portal-utils'; +import { getColumnCount } from '@portal/utils'; type GroupWrapperProps = { appGroups: AppGroup[]; diff --git a/client/packages/portal-ui/src/top-bar-actions/TopBarActionMenuDropdown.tsx b/client/packages/portal-ui/src/top-bar-actions/TopBarActionMenuDropdown.tsx index df5c5661b..491bd8fc5 100644 --- a/client/packages/portal-ui/src/top-bar-actions/TopBarActionMenuDropdown.tsx +++ b/client/packages/portal-ui/src/top-bar-actions/TopBarActionMenuDropdown.tsx @@ -1,7 +1,7 @@ import { Checkbox, Menu } from '@equinor/eds-core-react'; import { PortalAction, usePortalActions, useTopBarActions } from '@equinor/portal-core'; import { useEffect, useRef, useState } from 'react'; -import { TopBarAvatar } from '../avatar/TopBarAvatar'; +import { TopBarAvatar } from '@portal/components'; import { TopBarActionItem } from './TopBarActionItem'; import { StyledItem, StyledTopBarButton } from './TopBarActionStyles'; diff --git a/client/packages/portal-ui/src/top-bar-actions/TopBarActionStyles.ts b/client/packages/portal-ui/src/top-bar-actions/TopBarActionStyles.ts index cc26f17e9..ecb4e63fd 100644 --- a/client/packages/portal-ui/src/top-bar-actions/TopBarActionStyles.ts +++ b/client/packages/portal-ui/src/top-bar-actions/TopBarActionStyles.ts @@ -2,45 +2,46 @@ import { Button, Menu } from '@equinor/eds-core-react'; import styled from 'styled-components'; export const StyledMenuItem = styled(Menu.Item)` - min-width: 280px; - padding: 0px; - padding-left: 24px; - padding-right: 8px; - height: 48px; + min-width: 280px; + padding: 0px; + padding-left: 24px; + padding-right: 8px; + height: 48px; `; export const StyledItem = styled.div` - min-width: 250px; - padding-left: 0.5rem; + min-width: 250px; + padding-left: 0.5rem; `; export const StyledTopBarButton = styled(Button)` - height: 40px; - width: 40px; + height: 40px; + width: 40px; + margin-left: 0.25rem; `; export const StyledActionMenuButton = styled(Button)` - height: 48px; - width: 48px; + height: 48px; + width: 48px; `; export const StyledActionFavoriteButton = styled.span` - height: 48px; - width: 48px; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - height: 40px; - margin: 4px; - width: 40px; + height: 48px; + width: 48px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: 40px; + margin: 4px; + width: 40px; - :hover { - background: #fefefe; - border-radius: 50%; - } + :hover { + background: #fefefe; + border-radius: 50%; + } `; export const StyledActionListWrapper = styled.span` - display: flex; + display: flex; `; diff --git a/client/packages/types/README.md b/client/packages/types/README.md new file mode 100644 index 000000000..67e51e752 --- /dev/null +++ b/client/packages/types/README.md @@ -0,0 +1,31 @@ + + +
+
+ + + + +

Client - Project Types

+ +

+ This projects is just for global portal types. +
+ Explore Project Portal the docs +
+ Web Client Run-book +

+
+ + +## Project UI + +> [!NOTE] +> The project can be use like this, import component form `@portal/types` + +The is project should contain only types and no other code. + +[![typescript][typescript]][typescript-url] + +[typescript]: https://img.shields.io/badge/typescript-20232A?style=for-the-badge&logo=typescript&logoColor=61DAFB +[typescript-url]: https://typescriptlang.org diff --git a/client/packages/types/package.json b/client/packages/types/package.json new file mode 100644 index 000000000..d3abb8f7a --- /dev/null +++ b/client/packages/types/package.json @@ -0,0 +1,8 @@ +{ + "name": "@portal/types", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "build": "tsc" + } +} \ No newline at end of file diff --git a/client/packages/types/src/error/error.ts b/client/packages/types/src/error/error.ts new file mode 100644 index 000000000..340d94f63 --- /dev/null +++ b/client/packages/types/src/error/error.ts @@ -0,0 +1,16 @@ +export interface FusionError { + type: string; + title: string; + status: number; + instance: string; + error: BaseError; + traceId: string; + timestamp: string; +} + +interface BaseError { + resourceIdentifier: string; + code: string; + message: string; + possibleAction?: string | null; +} diff --git a/client/packages/types/src/error/index.ts b/client/packages/types/src/error/index.ts new file mode 100644 index 000000000..93ae819ea --- /dev/null +++ b/client/packages/types/src/error/index.ts @@ -0,0 +1 @@ +export * from './error'; diff --git a/client/packages/types/src/index.ts b/client/packages/types/src/index.ts new file mode 100644 index 000000000..9e2c500fa --- /dev/null +++ b/client/packages/types/src/index.ts @@ -0,0 +1,2 @@ +export * from './user'; +export * from './error'; diff --git a/client/packages/types/src/user/index.ts b/client/packages/types/src/user/index.ts new file mode 100644 index 000000000..6641fb6ac --- /dev/null +++ b/client/packages/types/src/user/index.ts @@ -0,0 +1,2 @@ +export * from './person-details'; +export * from './ui-types'; diff --git a/client/packages/types/src/user/person-details.ts b/client/packages/types/src/user/person-details.ts new file mode 100644 index 000000000..08b49619e --- /dev/null +++ b/client/packages/types/src/user/person-details.ts @@ -0,0 +1,75 @@ +type PersonBasePosition = { + id: string; + name: string; + discipline: string; +}; +export type PersonPosition = { + id: string; + name: string; + parentPositionId?: string; + obs: string; + project: PersonProject; + basePosition: PersonBasePosition; + positionId: string; + appliesFrom: Date | null; + appliesTo: Date | null; + workload: number | null; +}; + +type PersonProject = { + id: string; + name: string; + domainId: string; + type: string; +}; +type PersonContract = { + id: string; + name: string; + companyId?: string; + companyName: string; + contractNumber: string; + project: PersonProject; +}; +type PersonRoleScope = { + type: string; + value?: string; + valueType?: string; +}; + +export type PersonRole = { + name: string; + displayName: string; + sourceSystem: string; + type: string; + isActive: boolean; + activeToUtc?: string; + onDemandSupport: boolean; + scope: PersonRoleScope; +}; + +export type Role = PersonRole & { + errorMessage?: string; +}; + +type PersonAccountType = 'Consultant' | 'Employee' | 'External' | 'Local'; +type PersonCompany = { + id: string; + name: string; +}; +export type PersonDetails = { + azureUniqueId: string; + accountClassification: string; + name: string; + mail: string | null; + jobTitle: string | null; + department: string | null; + mobilePhone: string | null; + officeLocation: string | null; + upn: string; + managerAzureUniqueId: string; + accountType: PersonAccountType; + company: PersonCompany; + roles?: PersonRole[]; + contracts?: PersonContract[]; + positions?: PersonPosition[]; +}; diff --git a/client/packages/types/src/user/ui-types.ts b/client/packages/types/src/user/ui-types.ts new file mode 100644 index 000000000..e8d39aca5 --- /dev/null +++ b/client/packages/types/src/user/ui-types.ts @@ -0,0 +1,8 @@ +export const AccountColor = { + Consultant: '#eb0037', + Enterprise: '#eb0037', + Employee: '#8c1159', + External: '#ff92a8', + ExternalHire: '#000', + Default: '#ff92a8', +} as const; diff --git a/client/packages/portal-utils/tsconfig.json b/client/packages/types/tsconfig.json similarity index 100% rename from client/packages/portal-utils/tsconfig.json rename to client/packages/types/tsconfig.json diff --git a/client/packages/ui/.config/test-setup.ts b/client/packages/ui/.config/test-setup.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/client/packages/ui/.config/test-setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/client/packages/ui/README.md b/client/packages/ui/README.md new file mode 100644 index 000000000..7c92fc26e --- /dev/null +++ b/client/packages/ui/README.md @@ -0,0 +1,36 @@ + + +
+
+ + + + +

Client - Project UI

+ +

+ This projects is just ui components. +
+ Explore Project Portal the docs +
+ Web Client Run-book +

+
+ + +## Project UI + +> [!NOTE] +> The project can be use like this, import component form `@portal/ui` + +The is project should contain pure UI components, imports fro the `@portal/type` project is allowed but any other import should be dismissed in pull-requests are not allowed. + +[![typescript][typescript]][typescript-url] +[![React][react.js]][react-url] + + +[react.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB +[react-url]: https://reactjs.org/ + +[typescript]: https://img.shields.io/badge/typescript-20232A?style=for-the-badge&logo=typescript&logoColor=61DAFB +[typescript-url]: https://typescriptlang.org diff --git a/client/packages/ui/package.json b/client/packages/ui/package.json new file mode 100644 index 000000000..bb74a888f --- /dev/null +++ b/client/packages/ui/package.json @@ -0,0 +1,10 @@ +{ + "name": "@portal/ui", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "build": "tsc", + "test": "vitest --silent --run", + "test:coverage": "vitest run --coverage" + } +} \ No newline at end of file diff --git a/client/packages/ui/src/components/avatar/Avatar.test.tsx b/client/packages/ui/src/components/avatar/Avatar.test.tsx new file mode 100644 index 000000000..4a5fd8cb2 --- /dev/null +++ b/client/packages/ui/src/components/avatar/Avatar.test.tsx @@ -0,0 +1,46 @@ +import { describe, test, expect } from 'vitest'; +import { render } from '@testing-library/react'; +import Avatar from './Avatar'; +import '@testing-library/jest-dom'; + +describe('Avatar component', () => { + test('renders an avatar with a given URL', () => { + const imageUrl = 'https://example.com/avatar.png'; + + const { getByTestId } = render(); + + const avatarElement = getByTestId('avatar-icon'); + expect(avatarElement).toBeInTheDocument(); + expect(avatarElement).toHaveStyle(`background-image: url(${imageUrl})`); + }); + + test('renders a default avatar when URL is not provided', () => { + const { getByTestId } = render(); + + const avatarElement = getByTestId('avatar-icon'); + expect(avatarElement).toBeInTheDocument(); + expect(avatarElement).toHaveAttribute('aria-label', 'User Avatar'); + }); + + test('applies the specified border color', () => { + const imageUrl = 'https://example.com/avatar.png'; + const borderColor = '#FF0000'; + + const { getByTestId } = render(); + + const avatarElement = getByTestId('avatar-icon'); + expect(avatarElement).toHaveStyle(`border: 3px solid ${borderColor}`); + }); + + test('applies the specified width and height', () => { + const imageUrl = 'https://example.com/avatar.png'; + const width = 150; + const height = 150; + + const { getByTestId } = render(); + + const avatarElement = getByTestId('avatar-icon'); + expect(avatarElement).toHaveStyle(`width: ${width}px`); + expect(avatarElement).toHaveStyle(`height: ${height}px`); + }); +}); diff --git a/client/packages/ui/src/components/avatar/Avatar.tsx b/client/packages/ui/src/components/avatar/Avatar.tsx new file mode 100644 index 000000000..0bd0b47bb --- /dev/null +++ b/client/packages/ui/src/components/avatar/Avatar.tsx @@ -0,0 +1,55 @@ +import { Icon } from '@equinor/eds-core-react'; +import { account_circle } from '@equinor/eds-icons'; +import { tokens } from '@equinor/eds-tokens'; + +import styled from 'styled-components'; + +const Styles = { + photo: styled.div<{ height?: number; width?: number; borderColor?: `#${string}` }>` + height: ${({ height }) => (height ? `${height}px` : '32px')}; + width: ${({ width }) => (width ? `${width}px` : '32px')}; + overflow: hidden; + border-radius: 50%; + flex-shrink: 0; + position: relative; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + border: ${({ borderColor }) => (borderColor ? `3px solid ${borderColor}` : '2px solid none')}; + `, +}; +``; +type AvatarProps = { + url?: string; + borderColor?: `#${string}`; + height?: number; + width?: number; +}; + +export const Avatar = ({ url, width, height, borderColor }: AvatarProps): JSX.Element | null => { + if (!url) { + return ( + + ); + } + + return ( + + ); +}; + +export default Avatar; diff --git a/client/packages/ui/src/components/icons/DelveIcon.svg.tsx b/client/packages/ui/src/components/icons/DelveIcon.svg.tsx new file mode 100644 index 000000000..3541bb3a2 --- /dev/null +++ b/client/packages/ui/src/components/icons/DelveIcon.svg.tsx @@ -0,0 +1,61 @@ +export const DelveIcon = (props: Omit, 'xmlns' | 'viewBox'>) => ( + + {/* */} + + + + + + + + + + + + + + + + + + +); diff --git a/client/packages/ui/src/components/icons/TeamsIcon.svg.tsx b/client/packages/ui/src/components/icons/TeamsIcon.svg.tsx new file mode 100644 index 000000000..8a5984785 --- /dev/null +++ b/client/packages/ui/src/components/icons/TeamsIcon.svg.tsx @@ -0,0 +1,73 @@ +export const TeamsIcon = (props: Omit, 'xmlns' | 'viewBox'>) => ( + + + + + + + + + + + + + + + + + + + + + +); diff --git a/client/packages/ui/src/components/icons/index.ts b/client/packages/ui/src/components/icons/index.ts new file mode 100644 index 000000000..54d112356 --- /dev/null +++ b/client/packages/ui/src/components/icons/index.ts @@ -0,0 +1,2 @@ +export * from './DelveIcon.svg'; +export * from './TeamsIcon.svg'; diff --git a/client/packages/ui/src/components/skeleton/Skeleton.tsx b/client/packages/ui/src/components/skeleton/Skeleton.tsx new file mode 100644 index 000000000..6539f8d0b --- /dev/null +++ b/client/packages/ui/src/components/skeleton/Skeleton.tsx @@ -0,0 +1,46 @@ +import FusionSkeleton, { SkeletonSize, SkeletonVariant } from '@equinor/fusion-react-skeleton'; +import { CSSProperties, FC } from 'react'; + +type SkeletonProps = { + width?: number | string; + size?: keyof typeof skeletonSize; + variant?: keyof typeof skeletonVariant; + fluid?: boolean; +}; + +const skeletonVariant = { + circle: SkeletonVariant.Circle, + rectangle: SkeletonVariant.Rectangle, + square: SkeletonVariant.Square, + text: SkeletonVariant.Text, +}; + +const skeletonSize = { + xSmall: SkeletonSize.XSmall, + small: SkeletonSize.small, + large: SkeletonSize.Large, + medium: SkeletonSize.Medium, +}; + +/** + * Skeleton Component + * + * The `Skeleton` component is a simplified ree-export of `@equinor/fusion-react-skeleton` a React component used to render skeleton loading elements. + * + * @param width - number (optional) - Specifies the width of the skeleton element in present% + * @param type - string (optional) - Specifies the type of skeleton to render. Should be one of "xSmall" | "small" | "large" | "medium" default is xSmall. + * @param variant - string (optional) - Specifies the variant or shape of the skeleton. Should be one of "circle" | "rectangle" | "square" | "text", default is text. + * @param fluid - boolean (optional) - Expands the skeleton element width to the width of the parent + * + * @returns JSX.Element - A skeleton loading element with the specified type, variant, and width (if provided). + */ +export const Skeleton: FC = ({ width, size, variant, style, fluid }) => { + return ( + + ); +}; diff --git a/client/packages/ui/src/index.ts b/client/packages/ui/src/index.ts new file mode 100644 index 000000000..8ef267256 --- /dev/null +++ b/client/packages/ui/src/index.ts @@ -0,0 +1,7 @@ +// Export UI Components +export { Avatar } from './components/avatar/Avatar'; +export { Skeleton } from './components/skeleton/Skeleton'; +export * from './components/icons'; + +// Export UI Utils +export * from './utils/user'; diff --git a/client/packages/ui/src/utils/user.test.ts b/client/packages/ui/src/utils/user.test.ts new file mode 100644 index 000000000..9a849bb54 --- /dev/null +++ b/client/packages/ui/src/utils/user.test.ts @@ -0,0 +1,37 @@ +import { describe, test, expect } from 'vitest'; +import { getAccountTypeColor } from './user'; +import { AccountColor } from '@portal/types'; + +describe('getAccountTypeColor', () => { + test('should return the correct color for Employee', () => { + expect(getAccountTypeColor('Employee')).toEqual(AccountColor.Employee); + }); + + test('should return the correct color for Consultant', () => { + expect(getAccountTypeColor('Consultant')).toEqual(AccountColor.Consultant); + }); + + test('should return the correct color for Enterprise', () => { + expect(getAccountTypeColor('Enterprise')).toEqual(AccountColor.Enterprise); + }); + + test('should return the correct color for External', () => { + expect(getAccountTypeColor('External')).toEqual(AccountColor.External); + }); + + test('should return the correct color for External Hire', () => { + expect(getAccountTypeColor('External Hire')).toEqual(AccountColor.ExternalHire); + }); + + test('should return the correct color for null accountType', () => { + expect(getAccountTypeColor(null)).toEqual(AccountColor.Default); + }); + + test('should return the default color for an unknown account type', () => { + expect(getAccountTypeColor('UnknownType')).toEqual(AccountColor.Default); + }); + + test('should return the default color for an undefined accountType', () => { + expect(getAccountTypeColor()).toEqual(AccountColor.Default); + }); +}); diff --git a/client/packages/ui/src/utils/user.ts b/client/packages/ui/src/utils/user.ts new file mode 100644 index 000000000..d7308832f --- /dev/null +++ b/client/packages/ui/src/utils/user.ts @@ -0,0 +1,32 @@ +import { AccountColor } from '@portal/types'; + +/** + * Get the color associated with a specific account type. + * + * @param accountType - string | undefined - The account type for which to retrieve the color. + * @returns string - The color code associated with the provided account type. + * + * @example + * const color = getAccountTypeColor('Employee'); + * // Returns: '#8c1159' + * + * @example + * const color = getAccountTypeColor('UnknownType'); + * // Returns the default color: '#ff92a8' + */ +export const getAccountTypeColor = (accountType?: string | null) => { + switch (accountType) { + case 'Employee': + return AccountColor.Employee; + case 'Consultant': + return AccountColor.Consultant; + case 'Enterprise': + return AccountColor.Enterprise; + case 'External': + return AccountColor.External; + case 'External Hire': + return AccountColor.ExternalHire; + default: + return AccountColor.Default; + } +}; diff --git a/client/packages/ui/tsconfig.json b/client/packages/ui/tsconfig.json new file mode 100644 index 000000000..601ec8cb8 --- /dev/null +++ b/client/packages/ui/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "noFallthroughCasesInSwitch": true, + "outDir": "../../dist/out-tsc", + "types": ["node"], + + }, + "files": [], + + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "**/*.d.ts", "src/mock"] +} diff --git a/client/packages/ui/vite.config.ts b/client/packages/ui/vite.config.ts new file mode 100644 index 000000000..6f889a252 --- /dev/null +++ b/client/packages/ui/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import tsconfig from 'vite-tsconfig-paths'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tsconfig()], + test: { + environment: 'jsdom', + globals: true, + server: { + deps: { + fallbackCJS: true, + }, + }, + setupFiles: ['.config/test-setup.ts'], + }, + resolve: { + alias: { + 'portal-types': path.resolve(__dirname, '../types/src/index.ts'), + }, + }, +}); diff --git a/client/packages/portal-utils/README.md b/client/packages/utils/README.md similarity index 100% rename from client/packages/portal-utils/README.md rename to client/packages/utils/README.md diff --git a/client/packages/portal-utils/package.json b/client/packages/utils/package.json similarity index 82% rename from client/packages/portal-utils/package.json rename to client/packages/utils/package.json index 2f9ca913c..aeb34a06e 100644 --- a/client/packages/portal-utils/package.json +++ b/client/packages/utils/package.json @@ -1,5 +1,5 @@ { - "name": "@equinor/portal-utils", + "name": "@portal/utils", "version": "0.0.0", "license": "MIT", "scripts": { diff --git a/client/packages/portal-utils/src/index.ts b/client/packages/utils/src/index.ts similarity index 50% rename from client/packages/portal-utils/src/index.ts rename to client/packages/utils/src/index.ts index e12045988..faeed365b 100644 --- a/client/packages/portal-utils/src/index.ts +++ b/client/packages/utils/src/index.ts @@ -2,4 +2,6 @@ export * from './list-modifiers'; export * from './response-error-parser'; export * from './rxjs'; export * from './storage'; -export * from './menu-utils' +export * from './menu-utils'; +export * from './mutate-array/mutate-array'; +export * from './url/fusion-portal-url'; diff --git a/client/packages/portal-utils/src/list-modifiers/index.ts b/client/packages/utils/src/list-modifiers/index.ts similarity index 100% rename from client/packages/portal-utils/src/list-modifiers/index.ts rename to client/packages/utils/src/list-modifiers/index.ts diff --git a/client/packages/portal-utils/src/list-modifiers/move-item-by-index.test.ts b/client/packages/utils/src/list-modifiers/move-item-by-index.test.ts similarity index 100% rename from client/packages/portal-utils/src/list-modifiers/move-item-by-index.test.ts rename to client/packages/utils/src/list-modifiers/move-item-by-index.test.ts diff --git a/client/packages/portal-utils/src/list-modifiers/move-item-by-index.ts b/client/packages/utils/src/list-modifiers/move-item-by-index.ts similarity index 100% rename from client/packages/portal-utils/src/list-modifiers/move-item-by-index.ts rename to client/packages/utils/src/list-modifiers/move-item-by-index.ts diff --git a/client/packages/portal-utils/src/menu-utils/index.ts b/client/packages/utils/src/menu-utils/index.ts similarity index 100% rename from client/packages/portal-utils/src/menu-utils/index.ts rename to client/packages/utils/src/menu-utils/index.ts diff --git a/client/packages/portal-utils/src/menu-utils/utils.test.ts b/client/packages/utils/src/menu-utils/utils.test.ts similarity index 100% rename from client/packages/portal-utils/src/menu-utils/utils.test.ts rename to client/packages/utils/src/menu-utils/utils.test.ts diff --git a/client/packages/portal-utils/src/menu-utils/utils.ts b/client/packages/utils/src/menu-utils/utils.ts similarity index 100% rename from client/packages/portal-utils/src/menu-utils/utils.ts rename to client/packages/utils/src/menu-utils/utils.ts diff --git a/client/packages/utils/src/mutate-array/mutate-array.test.ts b/client/packages/utils/src/mutate-array/mutate-array.test.ts new file mode 100644 index 000000000..e22588f6a --- /dev/null +++ b/client/packages/utils/src/mutate-array/mutate-array.test.ts @@ -0,0 +1,51 @@ +import { mutateArray } from './mutate-array'; +import { it, describe, expect, beforeAll, vi } from 'vitest'; + +describe('mutateArray', () => { + const sampleArray = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ]; + + beforeAll(() => { + console.warn = vi.fn(); + }); + + it('should initialize correctly', () => { + const result = mutateArray(sampleArray, 'id'); + + expect(result.value).toEqual(sampleArray); + expect(result.getValue()).toEqual(sampleArray); + }); + + it('should mutate the array correctly', () => { + const result = mutateArray(sampleArray, 'id'); + + const mutatedResult = result.mutate((dict) => { + // Add a new item to the dictionary + dict['4'] = { id: 4, name: 'David' }; + return dict; + }); + + const expectedMutatedArray = [...sampleArray, { id: 4, name: 'David' }]; + + expect(mutatedResult.value).toEqual(expectedMutatedArray); + expect(mutatedResult.getValue()).toEqual(expectedMutatedArray); + }); + + it('should handle errors in the mutation callback', () => { + const result = mutateArray(sampleArray, 'id'); + + const error = new Error('Some error'); + + const mutatedResult = result.mutate(() => { + throw error; + }); + + expect(console.warn).toHaveBeenCalledWith(error); + + expect(mutatedResult.value).toEqual(sampleArray); + expect(mutatedResult.getValue()).toEqual(sampleArray); + }); +}); diff --git a/client/packages/utils/src/mutate-array/mutate-array.ts b/client/packages/utils/src/mutate-array/mutate-array.ts new file mode 100644 index 000000000..16f74d643 --- /dev/null +++ b/client/packages/utils/src/mutate-array/mutate-array.ts @@ -0,0 +1,63 @@ +/* eslint-disable tsdoc/syntax */ + +/** + * Mutate an array of objects into a dictionary-like structure, and provide a mechanism for safe mutations. + * + * @param array - An array of objects of type T to be converted into a dictionary. + * @param key - The key in the objects of the array to use as the dictionary key. + * @returns An object with the following properties: + * - `value` - An array of the original objects in the order they were provided. + * - `getValue` - A function that returns an array of the original objects in the same order. + * - `mutate` - A function to perform safe mutations on the dictionary. + * + * @typeparam T - The type of objects in the array. + * @typeparam K - The type of the key used for dictionary entries. + * + * @example + * const sampleArray = [ + * { id: 1, name: 'Alice' }, + * { id: 2, name: 'Bob' }, + * { id: 3, name: 'Charlie' }, + * ]; + * + * const result = mutateArray(sampleArray, 'id'); + * + * // Access the original array of objects + * const originalArray = result.value; // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }] + * + * // Access the original array using a function + * const originalArrayFunction = result.getValue(); // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }] + * + * // Mutate the dictionary using a callback + * const mutatedResult = result.mutate((dict) => { + * dict['4'] = { id: 4, name: 'David' }; + * return dict; + * }); + */ +export const mutateArray = (array: T[], key: K) => { + const dictionary: Record = array.reduce((acc, item) => { + const itemKey = item[key]; + if (typeof itemKey === 'object') { + acc[JSON.stringify(itemKey)] = item; + } else { + acc[String(itemKey)] = item; + } + return acc; + }, {} as Record); + + const mutate = (cb: (dict: Record) => Record | void) => { + try { + const mutatedDictionary = cb(dictionary); + return mutateArray(Object.values(mutatedDictionary || dictionary) as T[], key); + } catch (error) { + console.warn(error); + return mutateArray(Object.values(dictionary), key); + } + }; + + return { + value: Object.values(dictionary) as T[], + getValue: () => Object.values(dictionary) as T[], + mutate, + }; +}; diff --git a/client/packages/portal-utils/src/response-error-parser/index.ts b/client/packages/utils/src/response-error-parser/index.ts similarity index 100% rename from client/packages/portal-utils/src/response-error-parser/index.ts rename to client/packages/utils/src/response-error-parser/index.ts diff --git a/client/packages/portal-utils/src/response-error-parser/response-error-parser.test.ts b/client/packages/utils/src/response-error-parser/response-error-parser.test.ts similarity index 100% rename from client/packages/portal-utils/src/response-error-parser/response-error-parser.test.ts rename to client/packages/utils/src/response-error-parser/response-error-parser.test.ts diff --git a/client/packages/portal-utils/src/response-error-parser/response-error-parser.ts b/client/packages/utils/src/response-error-parser/response-error-parser.ts similarity index 100% rename from client/packages/portal-utils/src/response-error-parser/response-error-parser.ts rename to client/packages/utils/src/response-error-parser/response-error-parser.ts diff --git a/client/packages/portal-utils/src/rxjs/hooks/index.ts b/client/packages/utils/src/rxjs/hooks/index.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/hooks/index.ts rename to client/packages/utils/src/rxjs/hooks/index.ts diff --git a/client/packages/portal-utils/src/rxjs/hooks/use-observable.ts b/client/packages/utils/src/rxjs/hooks/use-observable.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/hooks/use-observable.ts rename to client/packages/utils/src/rxjs/hooks/use-observable.ts diff --git a/client/packages/portal-utils/src/rxjs/index.ts b/client/packages/utils/src/rxjs/index.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/index.ts rename to client/packages/utils/src/rxjs/index.ts diff --git a/client/packages/portal-utils/src/rxjs/observable-proxy.ts b/client/packages/utils/src/rxjs/observable-proxy.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/observable-proxy.ts rename to client/packages/utils/src/rxjs/observable-proxy.ts diff --git a/client/packages/portal-utils/src/rxjs/observable-storage.ts b/client/packages/utils/src/rxjs/observable-storage.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/observable-storage.ts rename to client/packages/utils/src/rxjs/observable-storage.ts diff --git a/client/packages/portal-utils/src/rxjs/types/index.ts b/client/packages/utils/src/rxjs/types/index.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/types/index.ts rename to client/packages/utils/src/rxjs/types/index.ts diff --git a/client/packages/portal-utils/src/rxjs/types/observable-proxy.ts b/client/packages/utils/src/rxjs/types/observable-proxy.ts similarity index 100% rename from client/packages/portal-utils/src/rxjs/types/observable-proxy.ts rename to client/packages/utils/src/rxjs/types/observable-proxy.ts diff --git a/client/packages/portal-utils/src/storage/index.ts b/client/packages/utils/src/storage/index.ts similarity index 100% rename from client/packages/portal-utils/src/storage/index.ts rename to client/packages/utils/src/storage/index.ts diff --git a/client/packages/portal-utils/src/storage/local-storage.ts b/client/packages/utils/src/storage/local-storage.ts similarity index 100% rename from client/packages/portal-utils/src/storage/local-storage.ts rename to client/packages/utils/src/storage/local-storage.ts diff --git a/client/packages/utils/src/url/fusion-portal-url.ts b/client/packages/utils/src/url/fusion-portal-url.ts new file mode 100644 index 000000000..816196bf9 --- /dev/null +++ b/client/packages/utils/src/url/fusion-portal-url.ts @@ -0,0 +1,13 @@ +export const getFusionPortalURL = () => { + switch (window._config_.fusionLegacyEnvIdentifier.toLowerCase()) { + case 'fprd': + return 'https://fusion.eqionor.com'; + case 'ci': + return 'https://fusion-s-portal-ci.azurewebsites.net'; + case 'fqa': + return 'https://fusion-s-portal-fqa.azurewebsites.net'; + + default: + return 'https://fusion-s-portal-ci.azurewebsites.net'; + } +}; diff --git a/client/packages/utils/tsconfig.json b/client/packages/utils/tsconfig.json new file mode 100644 index 000000000..a0c040a69 --- /dev/null +++ b/client/packages/utils/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "resolveJsonModule": true, + "noFallthroughCasesInSwitch": true, + "outDir": "../../dist/out-tsc", + "types": ["node"], + "allowJs": true, + "esModuleInterop": true, + }, + "files": [], + "exclude": [ + + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/client/tsconfig.base.json b/client/tsconfig.base.json index 52c004d89..85b609753 100644 --- a/client/tsconfig.base.json +++ b/client/tsconfig.base.json @@ -20,11 +20,15 @@ "@equinor/portal-modules": ["packages/portal-modules/src/index.ts"], "@equinor/portal-pages": ["packages/portal-pages/src/index.ts"], "@equinor/portal-ui": ["packages/portal-ui/src/index.ts"], - "@equinor/portal-utils": ["packages/portal-utils/src/index.ts"], + "@portal/utils": ["packages/utils/src/index.ts"], "@equinor/service-message": ["packages/service-message/index.ts"], - "@equinor/notification": ["packages/notification/index.ts"] + "@equinor/notification": ["packages/notification/index.ts"], + "@portal/types":["packages/types/src/index.ts"], + "@portal/core":["packages/core/src/index.ts"], + "@portal/ui":["packages/ui/src/index.ts"], + "@portal/components":["packages/components/src/index.ts"] } }, "include": ["dompurify.d.ts"], - "exclude": ["node_modules", "tmp", "dist"] + "exclude": ["node_modules", "tmp", "dist", "@testing-library/jest-dom"] } diff --git a/client/yarn.lock b/client/yarn.lock index 780865504..6468982da 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe/css-tools@^4.0.1": +"@adobe/css-tools@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== @@ -585,6 +585,16 @@ "@equinor/fusion-query" "^4.0.2" rxjs "^7.8.1" +"@equinor/fusion-framework-module-bookmark@^1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-module-bookmark/-/fusion-framework-module-bookmark-1.0.14.tgz#2776146f47d59337c05e7b72b91e38b6cc13a9dd" + integrity sha512-z+2clAZWOfH09ORqRR2kq1djK81Fr+8fyM8+89ux0myxyJyCGdAjUu4tdPbvyvikof8fS/KGcEodC22xieOLmA== + dependencies: + "@equinor/fusion-framework-module" "^4.2.5" + "@equinor/fusion-observable" "^8.1.2" + "@equinor/fusion-query" "^4.0.3" + rxjs "^7.8.1" + "@equinor/fusion-framework-module-context@^4.0.16": version "4.0.16" resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-module-context/-/fusion-framework-module-context-4.0.16.tgz#480f7d6525e5ebf09697625e4e81f17f93b9ee74" @@ -593,6 +603,14 @@ "@equinor/fusion-query" "^4.0.2" fast-deep-equal "^3.1.3" +"@equinor/fusion-framework-module-context@^4.0.17": + version "4.0.17" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-module-context/-/fusion-framework-module-context-4.0.17.tgz#be70e0f78ffc420f102ed24467ff43a855f7c5cd" + integrity sha512-v0rwpUWdSE5U3EQBLJb5hBuT0/C9JevaqjnFF3Owgsp1upwbjbigJRI/JYDtnHGc5blH5dWk0RhZ4Sd4c4sahQ== + dependencies: + "@equinor/fusion-query" "^4.0.3" + fast-deep-equal "^3.1.3" + "@equinor/fusion-framework-module-event@^4.0.6": version "4.0.6" resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-module-event/-/fusion-framework-module-event-4.0.6.tgz#31a71231e783d7c6187a929a08f59fc62ce6d797" @@ -634,6 +652,16 @@ "@equinor/fusion-query" "^4.0.2" rxjs "^7.8.1" +"@equinor/fusion-framework-module-service-discovery@^7.0.13": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-module-service-discovery/-/fusion-framework-module-service-discovery-7.0.13.tgz#37156b07d43f563bb6d12b5bfafd5f9dc43bf176" + integrity sha512-dMWTcItoFnlIU+VgJPEGNhzKG1t8bj1zWMmP3MtkKpHx7R4i/cSGbw3gkwSB4w1Za1sAjCHRfCqHxNa6CJYMvg== + dependencies: + "@equinor/fusion-framework-module" "^4.2.5" + "@equinor/fusion-framework-module-http" "^5.1.1" + "@equinor/fusion-query" "^4.0.3" + rxjs "^7.8.1" + "@equinor/fusion-framework-module-services@^3.2.2": version "3.2.2" resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-module-services/-/fusion-framework-module-services-3.2.2.tgz#3cd112e7eda178b8b11162483244946d3d2198f0" @@ -678,6 +706,18 @@ "@equinor/fusion-framework-react" "^5.3.2" "@equinor/fusion-framework-react-module-bookmark" "^2.0.20" +"@equinor/fusion-framework-react-components-people-provider@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-react-components-people-provider/-/fusion-framework-react-components-people-provider-1.1.2.tgz#d018fb4485901e2a1ce833856148663d3a178080" + integrity sha512-Gil+HHBHJjX7tDt6OuueRNNHM5+wZckY25Hq7wzAGz4HYy+kfMcT5MR6AAq4aJxWCJWSXLvXPrL/DpVPMKrOJg== + dependencies: + "@equinor/fusion-framework-module-services" "^3.2.2" + "@equinor/fusion-framework-react" "^5.3.3" + "@equinor/fusion-framework-react-module" "^3.0.6" + "@equinor/fusion-framework-react-module-bookmark" "^2.0.21" + "@equinor/fusion-query" "^4.0.3" + rxjs "^7.8.1" + "@equinor/fusion-framework-react-module-bookmark@^2.0.20": version "2.0.20" resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-react-module-bookmark/-/fusion-framework-react-module-bookmark-2.0.20.tgz#928196f0e5efd114fc8b3b073d95d2131e96c1fe" @@ -690,6 +730,18 @@ "@equinor/fusion-observable" "^8.1.2" "@equinor/fusion-query" "^4.0.2" +"@equinor/fusion-framework-react-module-bookmark@^2.0.21": + version "2.0.21" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-react-module-bookmark/-/fusion-framework-react-module-bookmark-2.0.21.tgz#28928b03771b369099a6b5aa8efdf90a7e67821e" + integrity sha512-n/z9ZIU9iaW2aqswxv06z1hXhmpyHQV04aMQdxT2lpUEEWKcSAQXY/Qy3uqbBG6J7I91pM2vSSBM5ZXOyv+iUw== + dependencies: + "@equinor/fusion-framework-module-bookmark" "^1.0.14" + "@equinor/fusion-framework-module-context" "^4.0.17" + "@equinor/fusion-framework-react" "^5.3.3" + "@equinor/fusion-framework-react-module" "^3.0.6" + "@equinor/fusion-observable" "^8.1.2" + "@equinor/fusion-query" "^4.0.3" + "@equinor/fusion-framework-react-module-http@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-react-module-http/-/fusion-framework-react-module-http-4.0.1.tgz#972af06346ba8af40860645396c70e941d640bd3" @@ -721,6 +773,18 @@ "@equinor/fusion-observable" "^8.1.2" rxjs "^7.8.1" +"@equinor/fusion-framework-react@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework-react/-/fusion-framework-react-5.3.3.tgz#7f7a0458f7977ff278dee91101be03ccc2be6857" + integrity sha512-0Q36BGjqGA/pZuDnEXuGIYR3tmU4afkFD6QOJfMWwsz3HGCKf5j9Ge7vPSNXPt6ApD4DhLR3bckN/alqdlewmQ== + dependencies: + "@equinor/fusion-framework" "^7.0.23" + "@equinor/fusion-framework-module" "^4.2.5" + "@equinor/fusion-framework-react-module" "^3.0.6" + "@equinor/fusion-framework-react-module-http" "^4.0.1" + "@equinor/fusion-observable" "^8.1.2" + rxjs "^7.8.1" + "@equinor/fusion-framework@^7.0.22": version "7.0.22" resolved "https://registry.yarnpkg.com/@equinor/fusion-framework/-/fusion-framework-7.0.22.tgz#294d6db103a7233e260eb35a1acc6adb9882d66a" @@ -735,6 +799,20 @@ "@equinor/fusion-framework-module-services" "^3.2.2" rxjs "^7.8.1" +"@equinor/fusion-framework@^7.0.23": + version "7.0.23" + resolved "https://registry.yarnpkg.com/@equinor/fusion-framework/-/fusion-framework-7.0.23.tgz#10a2633690c7c8b656f52c9846e295f05dc9d75c" + integrity sha512-jw1nk+8cPvbEFWLsE4dyTZcwWA4c9wiENT16gZ53JgIinrz5boGW1CbrNgrX+B5908mhIwDwPeTTz/GYkA2hXg== + dependencies: + "@equinor/fusion-framework-module" "^4.2.5" + "@equinor/fusion-framework-module-context" "^4.0.17" + "@equinor/fusion-framework-module-event" "^4.0.6" + "@equinor/fusion-framework-module-http" "^5.1.1" + "@equinor/fusion-framework-module-msal" "^3.0.7" + "@equinor/fusion-framework-module-service-discovery" "^7.0.13" + "@equinor/fusion-framework-module-services" "^3.2.2" + rxjs "^7.8.1" + "@equinor/fusion-observable@^8.1.2": version "8.1.2" resolved "https://registry.yarnpkg.com/@equinor/fusion-observable/-/fusion-observable-8.1.2.tgz#46d79e0964df5a791e95dbf69aab5f4f7ab6cb2e" @@ -755,6 +833,26 @@ uuid "^9.0.0" vitest "^0.34.2" +"@equinor/fusion-query@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@equinor/fusion-query/-/fusion-query-4.0.3.tgz#d123a9d2c42a392036085bc2871b0f9959f318d8" + integrity sha512-Aq+phWHU7MS1pU+eEg0QkDP+hbtod/TYJqCwW7/tL+Nr3k7bNCJocg1twOdGWe5OPfApcEsWAY9tfZL7ni7Xog== + dependencies: + "@equinor/fusion-observable" "^8.1.2" + immer "^9.0.16" + rxjs "^7.8.1" + uuid "^9.0.0" + vitest "^0.34.2" + +"@equinor/fusion-react-button@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-react-button/-/fusion-react-button-0.9.0.tgz#4fab802424014e36c548ce18b859af9d55b12974" + integrity sha512-dwdlj5q8cIrE4r0aKrR1TIx3yHYw+8Al0HeyKSlvy158WSBS4UXQ1gL7P7jVQQQ2GvK/IAmqAxdKdAVcXM3B3Q== + dependencies: + "@equinor/fusion-react-utils" "^2.1.0" + "@equinor/fusion-wc-button" "^2.1.0" + "@equinor/fusion-wc-icon" "^1.0.24" + "@equinor/fusion-react-context-selector@^0.4.9": version "0.4.9" resolved "https://registry.yarnpkg.com/@equinor/fusion-react-context-selector/-/fusion-react-context-selector-0.4.9.tgz#d51972bf662f829d831e952ec228daac76f0a22d" @@ -773,6 +871,15 @@ "@equinor/fusion-react-utils" "^2.0.5" "@equinor/fusion-wc-icon" "^1.0.24" +"@equinor/fusion-react-person@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-react-person/-/fusion-react-person-0.6.0.tgz#c02a5b64b81646da2a016b3e1deb454845eb51a3" + integrity sha512-XyFCAmVFhSPXt/6LFKMVxpJjCFIwVkPhLg+WMl6huOXhNudUDZ2HSf+nztIG7klFecIxgSJ1TYiNL/MiJOsaeg== + dependencies: + "@equinor/fusion-react-button" "^0.9.0" + "@equinor/fusion-react-utils" "^2.1.0" + "@equinor/fusion-wc-person" "^2.0.0" + "@equinor/fusion-react-searchable-dropdown@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@equinor/fusion-react-searchable-dropdown/-/fusion-react-searchable-dropdown-0.3.5.tgz#edb65256e2075d500a36603f2eb71847590325c5" @@ -821,6 +928,47 @@ dependencies: date-fns "^2.28.0" +"@equinor/fusion-react-utils@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-react-utils/-/fusion-react-utils-2.1.0.tgz#347d9ceffb48d727110eeed7ff3fa94ff8d8bfbd" + integrity sha512-qbQ8Unf8uX5+oVNOBRXq14eTRIiMr2GmShbzYcuZyHlH1Ri4zKns7Lzas/IjbUq/Wxv7uG0A2ssKZX2VipdZXA== + dependencies: + date-fns "^2.28.0" + +"@equinor/fusion-wc-avatar@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-avatar/-/fusion-wc-avatar-3.1.1.tgz#53d4ff592708373a6a4ab46016a99a80c2f252f8" + integrity sha512-4OMfn5hoHN5urBetxrN/4h7rgxj7sisb2P0NldkQAhfGTJN9hlv6Q2xOpa3x2zhF0zYdtLVeYim7pATfVd+NGQ== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-picture" "^3.0.0" + "@equinor/fusion-wc-ripple" "^1.0.0" + "@equinor/fusion-web-theme" "^0.1.7" + lit "2.7.0" + +"@equinor/fusion-wc-badge@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-badge/-/fusion-wc-badge-1.2.1.tgz#b556f14b59160d606ade554255f2356d72ac208e" + integrity sha512-Yr7tQal+qIVmpBtXNzKMx9E26fiBkCzaGadUF2WucDgta3J/YCazAYciAUR5O2tHXcxjrqj2AvYAleXxMSVhuQ== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-icon" "^2.2.0" + "@equinor/fusion-web-theme" "^0.1.7" + lit "2.7.0" + +"@equinor/fusion-wc-button@^2.1.0", "@equinor/fusion-wc-button@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-button/-/fusion-wc-button-2.2.0.tgz#e852ca04f0ce0bb485c07cbf671cd7075700de52" + integrity sha512-BZ1pLwKZByLVe8PN8Lkgmeq8vQe5QQBxC6vfQO2o0fVLyK1BN637YlaMtYxF9CGm3i0C1pSJ0JZVW2zLFCLuMQ== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-icon" "^2.2.0" + "@equinor/fusion-web-theme" "^0.1.7" + "@material/mwc-button" "^0.27.0" + "@material/mwc-icon-button" "^0.27.0" + "@material/mwc-icon-button-toggle" "^0.27.0" + lit "2.7.0" + "@equinor/fusion-wc-checkbox@^0.3.15": version "0.3.15" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-checkbox/-/fusion-wc-checkbox-0.3.15.tgz#44045aac331f29ac4a38d7391963e6c469ff3790" @@ -830,11 +978,26 @@ "@material/mwc-checkbox" "^0.27.0" lit "2.7.0" +"@equinor/fusion-wc-checkbox@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-checkbox/-/fusion-wc-checkbox-1.0.1.tgz#9ee064bcd8d053bf37bc8d4a9c5fd2e9a2a72f1c" + integrity sha512-tvwl1a4xkOW2MbATru950C5OybyeHPvOPNdfAksJtX8KjyK1Ph98AUVdtDlfnHloO3JJbfPT8xO+TMzjHE19bw== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-web-theme" "^0.1.7" + "@material/mwc-checkbox" "^0.27.0" + lit "2.7.0" + "@equinor/fusion-wc-core@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-core/-/fusion-wc-core-1.0.5.tgz#46db3a53bd4b4dbed32887c1a7e25104b4a7493b" integrity sha512-VKvsJQqiyBjz/qTGirSiJDrwHYc0rXyIXa5VtaGkeQ13bSxrGM1djOwvqMjkeNh+zImBR27yDqqBz+8zvBIxvw== +"@equinor/fusion-wc-core@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-core/-/fusion-wc-core-2.0.0.tgz#ac362bd4d97a54d19f334630e6bd732b511bb190" + integrity sha512-yxUepfQhMIkKNYn2Vc73R6/cyvyO0hde3LXFitCCaa8QR9woK0UXZlxP+RwnEYzve528l9K1t7SnEsX6x5eyPg== + "@equinor/fusion-wc-divider@^0.2.21": version "0.2.21" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-divider/-/fusion-wc-divider-0.2.21.tgz#a24979511e7d020f1f68a37f35d8ee874c8af75d" @@ -843,6 +1006,15 @@ "@equinor/fusion-wc-core" "^1.0.5" lit "2.7.0" +"@equinor/fusion-wc-divider@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-divider/-/fusion-wc-divider-1.0.0.tgz#69ced8d0cc9f57c5677488d83a8b6c32f9d80913" + integrity sha512-KwU9PMsywo6u4O5peMAqieMC4ZEWcB2ISdJMWcFgiSiNOxgGWMuMjQ22Ex75OWG7aMV4g3ZHFPaC4dzJzUd5Dg== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-web-theme" "^0.1.7" + lit "2.7.0" + "@equinor/fusion-wc-icon@^1.0.24", "@equinor/fusion-wc-icon@^1.0.31": version "1.0.31" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-icon/-/fusion-wc-icon-1.0.31.tgz#d9ccaf5b17fd970a174f2f6e73798d95b2d4e2db" @@ -852,6 +1024,15 @@ "@equinor/fusion-wc-core" "^1.0.5" lit "2.7.0" +"@equinor/fusion-wc-icon@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-icon/-/fusion-wc-icon-2.2.0.tgz#020d813181a7c9a4fa44f39e4c8a0d03df057bfd" + integrity sha512-gcklHv0tdDBeshqyaJ9A9uDefXyO2kOIMITLo3nlXscZOh0Lm3a0We6tpQPE0/Cl8TLwGY/6Faf19DpdXpcHWQ== + dependencies: + "@equinor/eds-icons" "^0.19.1" + "@equinor/fusion-wc-core" "^2.0.0" + lit "2.7.0" + "@equinor/fusion-wc-list@^0.3.5": version "0.3.5" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-list/-/fusion-wc-list-0.3.5.tgz#5aed53790a796be4e9bdff503e7f6236a2e80292" @@ -865,6 +1046,48 @@ "@material/mwc-list" "^0.27.0" lit "2.7.0" +"@equinor/fusion-wc-list@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-list/-/fusion-wc-list-1.0.3.tgz#70059fa816a932741d3b10ec9713648c0c24715e" + integrity sha512-Dp+USzneOBbLlG7TBlOHdBB7/DLCcXUTF2yQafZMk0+I0mXO6qECh+lwX8pXYlr7D1jCRIAcB7NmxZMq2yA6tg== + dependencies: + "@equinor/fusion-wc-checkbox" "^1.0.1" + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-divider" "^1.0.0" + "@equinor/fusion-wc-icon" "^2.2.0" + "@equinor/fusion-wc-radio" "^1.0.0" + "@equinor/fusion-web-theme" "^0.1.7" + "@material/mwc-list" "^0.27.0" + lit "2.7.0" + +"@equinor/fusion-wc-person@^2.0.0": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-person/-/fusion-wc-person-2.1.4.tgz#60e0280367ac538e62893ffbd22a3f6d9dec37f8" + integrity sha512-0YJaaqrmK3IcQIV3w0f8HLwgeZlA9M/BXF3ehtDnCxItmnHnLzLBUIF1nWZD1GHzyNwV+R2HsU5E+ArLWJ0OvA== + dependencies: + "@equinor/fusion-wc-avatar" "^3.1.1" + "@equinor/fusion-wc-badge" "^1.2.1" + "@equinor/fusion-wc-button" "^2.2.0" + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-icon" "^2.2.0" + "@equinor/fusion-wc-list" "^1.0.3" + "@equinor/fusion-wc-searchable-dropdown" "^3.4.0" + "@equinor/fusion-wc-skeleton" "^2.0.0" + "@equinor/fusion-wc-textinput" "^1.0.2" + "@equinor/fusion-web-theme" "^0.1.7" + "@floating-ui/dom" "^1.3.0" + "@lit-labs/observers" "^2.0.0" + "@lit-labs/task" "^3.0.1" + lit "2.7.0" + +"@equinor/fusion-wc-picture@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-picture/-/fusion-wc-picture-3.0.0.tgz#50b230bfecda331097bc65dbf5f3629a546eeac1" + integrity sha512-dtG/Md07y4I+M2s1EiQCVdAyI7drcQDjxyD3fDzWvtykkBn5xevsNOliGYtSnzZY+T8AAUwvMDjpLPK3LlQEHA== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + lit "2.7.0" + "@equinor/fusion-wc-radio@^0.2.25": version "0.2.25" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-radio/-/fusion-wc-radio-0.2.25.tgz#c7f5b18ee695cae41f61221ceada60ed5045221d" @@ -874,6 +1097,25 @@ "@material/mwc-radio" "^0.27.0" lit "2.7.0" +"@equinor/fusion-wc-radio@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-radio/-/fusion-wc-radio-1.0.0.tgz#67a3142bd8f4850015b7de4cc7ff65c03009fe41" + integrity sha512-YQHGX9REqgb8WCND1xHgFfHniMJZhTvQuh2gm64EMxfzid8lcKHTloQ4kVKljUeFa3JEKTtxhYWiPfSCCoTPcg== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-web-theme" "^0.1.7" + "@material/mwc-radio" "^0.27.0" + lit "2.7.0" + +"@equinor/fusion-wc-ripple@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-ripple/-/fusion-wc-ripple-1.0.0.tgz#afe19e9deb019d90482091af9cfe44aec462b8d8" + integrity sha512-CjH5s7KVi0Byok+b/bNhMelqa12Ub9NBy+KY+JrFz/ynuTtK+XR3etE31RAxVE4ZM9T+4dMVIU+S9OdgcqxHlQ== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@material/mwc-ripple" "^0.27.0" + lit "2.7.0" + "@equinor/fusion-wc-searchable-dropdown@^2.5.1": version "2.5.3" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-searchable-dropdown/-/fusion-wc-searchable-dropdown-2.5.3.tgz#8f0d1053d342bd335c7bc18a915b04a3a7ef09ad" @@ -889,6 +1131,23 @@ lit "2.7.0" uuid "^9.0.0" +"@equinor/fusion-wc-searchable-dropdown@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-searchable-dropdown/-/fusion-wc-searchable-dropdown-3.4.0.tgz#934c459de97edb867bcb3c1ba868b650cfe7cb94" + integrity sha512-jqSmNks1bBlWuNveGBP3yOwhv0WU+gQklSe8JwWT6kfmEtef+kaDpaPRtwoA+aPY09mxQkSVxrXC2HhyJ3aDJg== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-divider" "^1.0.0" + "@equinor/fusion-wc-icon" "^2.2.0" + "@equinor/fusion-wc-list" "^1.0.3" + "@equinor/fusion-wc-textinput" "^1.0.2" + "@equinor/fusion-web-theme" "^0.1.7" + "@lit-labs/task" "^2.1.0" + "@material/mwc-textfield" "^0.27.0" + "@types/uuid" "^9.0.1" + lit "2.7.0" + uuid "^9.0.0" + "@equinor/fusion-wc-skeleton@^0.2.17": version "0.2.24" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-skeleton/-/fusion-wc-skeleton-0.2.24.tgz#d6b4c94edf2d1201cd365053c2132393db7498f1" @@ -897,6 +1156,15 @@ "@equinor/fusion-wc-core" "^1.0.5" lit "2.7.0" +"@equinor/fusion-wc-skeleton@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-skeleton/-/fusion-wc-skeleton-2.0.0.tgz#48e2e5d5931825a9a75f8614257a9d88c0518c16" + integrity sha512-qELLw/7yOAucMhc3MDYBfjJcvXOIi4OupsS7127sAFe/MjvY9IDRURQYERCebyOIUFyLX40XOs8mXfIKRh3tsA== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-web-theme" "^0.1.7" + lit "2.7.0" + "@equinor/fusion-wc-textinput@^0.5.19": version "0.5.19" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-textinput/-/fusion-wc-textinput-0.5.19.tgz#b08f62de7dbe9ed77e1e57c0ab6267e15817c79d" @@ -907,6 +1175,17 @@ "@material/mwc-textfield" "^0.27.0" lit "2.7.0" +"@equinor/fusion-wc-textinput@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-textinput/-/fusion-wc-textinput-1.0.2.tgz#238849ec541dd650b4b8b9e005851f240809f112" + integrity sha512-z2kFZSX0VTVcdFJoWGDRQF2KRsQRo+PRcUlu+Fm2Ei1gBxwadTxL2Hpg9mke4bAox7iBRSZwMD9R9d/ACQ+cQQ== + dependencies: + "@equinor/fusion-wc-core" "^2.0.0" + "@equinor/fusion-wc-icon" "^2.2.0" + "@equinor/fusion-web-theme" "^0.1.7" + "@material/mwc-textfield" "^0.27.0" + lit "2.7.0" + "@equinor/fusion-wc-theme@^0.2.33": version "0.2.33" resolved "https://registry.yarnpkg.com/@equinor/fusion-wc-theme/-/fusion-wc-theme-0.2.33.tgz#0550831afd950340d534bea5ba0e8f06cffcafb5" @@ -1074,6 +1353,13 @@ dependencies: "@floating-ui/utils" "^0.1.1" +"@floating-ui/core@^1.4.2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.0.tgz#5c05c60d5ae2d05101c3021c1a2a350ddc027f8c" + integrity sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg== + dependencies: + "@floating-ui/utils" "^0.1.3" + "@floating-ui/dom@^1.2.1": version "1.5.1" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.1.tgz#88b70defd002fe851f17b4a25efb2d3c04d7a8d7" @@ -1082,6 +1368,14 @@ "@floating-ui/core" "^1.4.1" "@floating-ui/utils" "^0.1.1" +"@floating-ui/dom@^1.3.0": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa" + integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA== + dependencies: + "@floating-ui/core" "^1.4.2" + "@floating-ui/utils" "^0.1.3" + "@floating-ui/react-dom-interactions@^0.10.1": version "0.10.3" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.3.tgz#1d988aad169bf752b54c688db942f12e4fed61c5" @@ -1102,6 +1396,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.1.tgz#1a5b1959a528e374e8037c4396c3e825d6cf4a83" integrity sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw== +"@floating-ui/utils@^0.1.3": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" + integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== + "@hirez_io/observer-spy@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@hirez_io/observer-spy/-/observer-spy-2.2.0.tgz#3b1df531ba333a9a6cbbec0333d3fbdc4c68cc3c" @@ -1317,11 +1616,23 @@ yargs "16.2.0" yargs-parser "20.2.4" +"@lit-labs/observers@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@lit-labs/observers/-/observers-2.0.1.tgz#5bc5247bb3ab9205e3c29a5e01180a83c9780504" + integrity sha512-abzQfdzWfFFh+jA6BIVUaZ6tb2YDCkzysvlx0lhXFLvjc2IE+8p3GqbT8DD698KLhMa0NH9qLJxXXhUhSXrZcQ== + dependencies: + "@lit/reactive-element" "^2.0.0" + "@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9" integrity sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ== +"@lit-labs/ssr-dom-shim@^1.1.2-pre.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312" + integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g== + "@lit-labs/task@^2.1.0": version "2.1.2" resolved "https://registry.yarnpkg.com/@lit-labs/task/-/task-2.1.2.tgz#e7e44998edd1441f0d4f2101abe504da6e1f7c8e" @@ -1329,6 +1640,20 @@ dependencies: "@lit/reactive-element" "^1.1.0" +"@lit-labs/task@^3.0.1": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@lit-labs/task/-/task-3.1.0.tgz#4821d2d5e0e0af0e18e57136bb39c2662811cbd9" + integrity sha512-zMlcUtZeHDT83IiT2+CJBSoFvWDLnPEezhOCgqjxW4DmRHlbgd7jdft97T6dw4S4RvIETfI7OOyvubCV/EzTlg== + dependencies: + "@lit/task" "^1.0.0" + +"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.0.tgz#da14a256ac5533873b935840f306d572bac4a2ab" + integrity sha512-wn+2+uDcs62ROBmVAwssO4x5xue/uKD3MGGZOXL2sMxReTRIT0JXKyMXeu7gh0aJ4IJNEIG/3aOnUaQvM7BMzQ== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.1.2-pre.0" + "@lit/reactive-element@^1.1.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": version "1.6.3" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03" @@ -1336,6 +1661,13 @@ dependencies: "@lit-labs/ssr-dom-shim" "^1.0.0" +"@lit/task@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@lit/task/-/task-1.0.0.tgz#61ae9ac6131368bbcf5f09ccade8037e6bb8705e" + integrity sha512-7jocGBh3yGlo3kKxQggZph2txK4X5GYNWp2FAsmV9u2spzUypwrzRzXe8I72icAb02B00+k2nlvxVcrQB6vyrw== + dependencies: + "@lit/reactive-element" "^1.0.0 || ^2.0.0" + "@material-ui/styles@^4.11.4": version "4.11.5" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.5.tgz#19f84457df3aafd956ac863dbe156b1d88e2bbfb" @@ -1487,6 +1819,16 @@ lit "^2.0.0" tslib "^2.0.1" +"@material/mwc-button@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@material/mwc-button/-/mwc-button-0.27.0.tgz#192ab82eab8f7f4dcbf987d0af7bb808408d77d2" + integrity sha512-t5m2zfE93RNKHMjdsU67X6csFzuSG08VJKKvXVQ+BriGE3xBgzY5nZdmZXomFpaWjDENPAlyS4ppCFm6o+DILw== + dependencies: + "@material/mwc-icon" "^0.27.0" + "@material/mwc-ripple" "^0.27.0" + lit "^2.0.0" + tslib "^2.0.1" + "@material/mwc-checkbox@^0.27.0": version "0.27.0" resolved "https://registry.yarnpkg.com/@material/mwc-checkbox/-/mwc-checkbox-0.27.0.tgz#a299252df96045e9c37a67ff0d5c3f3469f27ae4" @@ -1506,6 +1848,34 @@ lit "^2.0.0" tslib "^2.0.1" +"@material/mwc-icon-button-toggle@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@material/mwc-icon-button-toggle/-/mwc-icon-button-toggle-0.27.0.tgz#1cbd3c59800f8fa0bb03a4efcf9f926c36b28012" + integrity sha512-PLozOm8m96vfPqTvugdCSC6V+sd337rdm/H2ZpnMbV+8gllfIkm1J02hv7rQ1O51ThrpMt4hd0URitIiEeuATg== + dependencies: + "@material/mwc-base" "^0.27.0" + "@material/mwc-icon-button" "^0.27.0" + "@material/mwc-ripple" "^0.27.0" + lit "^2.0.0" + tslib "^2.0.1" + +"@material/mwc-icon-button@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@material/mwc-icon-button/-/mwc-icon-button-0.27.0.tgz#c7e6ba3c8130b017ced00c311c6667a6c7d6d569" + integrity sha512-wReiPa1UkLaCSPtpkAs1OGKEBtvqPnz9kzuY+RvN5ZQnpo3Uh7n3plHV4y/stsUBfrWtBCcOgYnCdNRaR/r2nQ== + dependencies: + "@material/mwc-ripple" "^0.27.0" + lit "^2.0.0" + tslib "^2.0.1" + +"@material/mwc-icon@^0.27.0": + version "0.27.0" + resolved "https://registry.yarnpkg.com/@material/mwc-icon/-/mwc-icon-0.27.0.tgz#f25ecdd7fe54d810d7d655ba05251169f78b84fd" + integrity sha512-Sul44I37M9Ewynn0A9DjkEBrmll2VtNbth6Pxj7I1A/EAwEfaCrPvryyGqfIu1T2hTsRcaojzQx6QjF+B5QW9A== + dependencies: + lit "^2.0.0" + tslib "^2.0.1" + "@material/mwc-line-ripple@^0.27.0": version "0.27.0" resolved "https://registry.yarnpkg.com/@material/mwc-line-ripple/-/mwc-line-ripple-0.27.0.tgz#91fdc3b2fc3a2cc93cedbbf2342dd8bab37a60a6" @@ -2204,14 +2574,13 @@ lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c" - integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg== +"@testing-library/jest-dom@^6.1.4": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz#cf0835c33bc5ef00befb9e672b1e3e6a710e30e3" + integrity sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw== dependencies: - "@adobe/css-tools" "^4.0.1" + "@adobe/css-tools" "^4.3.1" "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" aria-query "^5.0.0" chalk "^3.0.0" css.escape "^1.5.1" @@ -2479,7 +2848,7 @@ "@types/react" "*" csstype "^3.0.2" -"@types/testing-library__jest-dom@^5.14.9", "@types/testing-library__jest-dom@^5.9.1": +"@types/testing-library__jest-dom@^5.14.9": version "5.14.9" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz#0fb1e6a0278d87b6737db55af5967570b67cb466" integrity sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==