diff --git a/.changeset/pr-901-2241966290.md b/.changeset/pr-901-2241966290.md new file mode 100644 index 000000000..37bf88f1f --- /dev/null +++ b/.changeset/pr-901-2241966290.md @@ -0,0 +1,5 @@ + +--- +"fusion-project-portal": patch +--- +Add assistance description to Incident type and enhance HelpMenu with help request option. diff --git a/client/packages/components/src/components/help/HelpMenu.tsx b/client/packages/components/src/components/help/HelpMenu.tsx index 0484843a6..58a01dc65 100644 --- a/client/packages/components/src/components/help/HelpMenu.tsx +++ b/client/packages/components/src/components/help/HelpMenu.tsx @@ -54,6 +54,15 @@ export const HelpMenu = ({ setActiveActionById }: { setActiveActionById: (id: st > Report an error + { + setActiveActionById('services'); + }} + > + Need Help? + { - const [activeTab, setTab] = useState<'ActiveIncident' | 'NewIncident'>('ActiveIncident'); + const [activeTab, setTab] = useState<'ActiveIncident' | 'NewIncident' | 'HelpNeeded'>('ActiveIncident'); const tabs = useMemo( () => ({ @@ -20,6 +20,9 @@ export const ServiceNow = () => { openNewIncident={() => { setTab('NewIncident'); }} + openNeedHelp={() => { + setTab('HelpNeeded'); + }} /> ), NewIncident: ( @@ -29,6 +32,13 @@ export const ServiceNow = () => { }} /> ), + HelpNeeded: ( + { + setTab('ActiveIncident'); + }} + /> + ), }), [] ); diff --git a/client/packages/components/src/components/service-now/components/ActiveIncidents.tsx b/client/packages/components/src/components/service-now/components/ActiveIncidents.tsx index 1d1071c12..7bfd5c1ab 100644 --- a/client/packages/components/src/components/service-now/components/ActiveIncidents.tsx +++ b/client/packages/components/src/components/service-now/components/ActiveIncidents.tsx @@ -5,6 +5,7 @@ import { Tooltip } from '@equinor/fusion-react-tooltip'; import { ActiveIncidentsList } from './ActiveIncidentsList'; import { info_circle } from '@equinor/eds-icons'; import { ActiveIncidentStateTooltip } from './ActiveIncidentStateTooltip'; +import InfoMessage from './InfoMessage'; const Styles = { Wrapper: styled.div` @@ -21,25 +22,26 @@ const Styles = { HeadingWrapper: styled.div` display: flex; justify-content: space-between; + padding-bottom: 1rem; `, }; type ActiveIncidentsProps = { openNewIncident: () => void; + openNeedHelp: () => void; }; -export const ActiveIncidents = ({ openNewIncident }: ActiveIncidentsProps): JSX.Element => { +export const ActiveIncidents = ({ openNewIncident, openNeedHelp }: ActiveIncidentsProps): JSX.Element => { return ( - My active Fusion incidents - }> - - + We are here to help - - + + @@ -53,6 +55,14 @@ export const ActiveIncidents = ({ openNewIncident }: ActiveIncidentsProps): JSX. Submit an improvement suggestion + + + My active Fusion incidents + }> + + + + ); }; diff --git a/client/packages/components/src/components/service-now/components/Help.tsx b/client/packages/components/src/components/service-now/components/Help.tsx new file mode 100644 index 000000000..4eecacf46 --- /dev/null +++ b/client/packages/components/src/components/service-now/components/Help.tsx @@ -0,0 +1,222 @@ +import { TextField, Button, Typography, CircularProgress, Icon } from '@equinor/eds-core-react'; +import { useForm, SubmitHandler } from 'react-hook-form'; + +import styled from 'styled-components'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { FileUpload } from './file-upload/FileUpload'; +import { useCreateServiceNowIncidents, useUploadAttachmentsServiceNowIncidents } from '../hooks/use-service-now-query'; +import { useIncidentMeta } from '../hooks/use-incident-meta'; + +import { useEffect, useState } from 'react'; +import { error_filled } from '@equinor/eds-icons'; +import { MessageCard } from '@portal/ui'; +import { UploadStatus } from '../types/types'; +import { AttachmentsApiFailed } from './AttachmentsApiFailed'; +import { AttachmentsPartialFail } from './AttachmentsPartialFail'; +import { HelpInput, Inputs, inputSchema } from '../schema'; +import InfoMessage from './InfoMessage'; +import { tokens } from '@equinor/eds-tokens'; + +type HelpNeededProps = { + onClose: () => void; +}; + +const Style = { + Wrapper: styled.div` + padding-left: 0.5rem; + padding-right: 0.5rem; + `, + From: styled.form` + padding-top: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + `, + ErrorWrapper: styled.div` + padding-top: 1rem; + padding-bottom: 1rem; + `, + ButtonWrapper: styled.div` + display: flex; + gap: 1rem; + `, +}; + +const formatDescription = (description: string) => { + return ` + Type: I need help\n\nWhat do you need assistance with?\n${description}\n\n + `; +}; + +export const HelpNeeded = ({ onClose }: HelpNeededProps) => { + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isSubmitSuccessful }, + reset, + watch, + setError, + clearErrors, + } = useForm({ + resolver: zodResolver(inputSchema), + }); + + const { + data: incident, + mutateAsync: createIncident, + error: createIncidentError, + reset: resetCreate, + } = useCreateServiceNowIncidents(); + + const { mutateAsync: uploadFiles, error: uploadError } = useUploadAttachmentsServiceNowIncidents(); + + const [uploadFilesErrors, setUploadFilesErrors] = useState(); + + const metadata = useIncidentMeta(); + + const onSubmit: SubmitHandler = async ({ shortDescription, description, files }) => { + const incident = await createIncident({ + shortDescription, + description: formatDescription(description), + metadata, + }); + + const filesArray = Array.from(files) as File[]; + if (filesArray.length > 0 && incident?.id) { + const uploadStatus = await uploadFiles({ files: filesArray, incidentId: incident.id }); + if (uploadStatus.status !== 'Error') { + setUploadFilesErrors(uploadStatus); + } + } + + if (incident) { + reset(); + } + }; + + useEffect(() => { + if (isSubmitSuccessful) { + onClose(); + } + }, [isSubmitSuccessful]); + + if (uploadFilesErrors?.status === 'Error' || uploadError) { + return ( + { + onClose(); + }} + /> + ); + } + if (uploadFilesErrors?.status === 'Waring') { + return ( + { + onClose(); + }} + /> + ); + } + + return ( + + I need help + + + {Object.values(errors).length > 0 && ( + + error.message?.toString() || '')} + /> + + )} + + {createIncidentError && ( + + + + + + + + + )} + + + } + label="Short description *" + placeholder="Ticket title, please keep short and concise" + maxLength={51} + required + /> + + } + multiline + rows={5} + required + /> + + { + clearErrors(); + + if (files.every((file) => file.type === 'image/jpeg' || file.type === 'image/png')) { + reset({ files }); + return; + } + setError('files', { + message: 'One or more files not supported, supported file are jpeg and png', + }); + }} + onRemoved={(files) => { + clearErrors(); + reset({ files }); + }} + /> + + NB: Check that you capture the entire screen when uploading a screenshot + + + + + + ); +}; diff --git a/client/packages/components/src/components/service-now/components/InfoMessage.tsx b/client/packages/components/src/components/service-now/components/InfoMessage.tsx new file mode 100644 index 000000000..ee7e835da --- /dev/null +++ b/client/packages/components/src/components/service-now/components/InfoMessage.tsx @@ -0,0 +1,36 @@ +import { Typography } from '@equinor/eds-core-react'; +import { tokens } from '@equinor/eds-tokens'; +import { ReactNode } from 'react'; +import styled from 'styled-components'; + +const Styled = { + InfoBar: styled.div` + display: flex; + padding: ${tokens.spacings.comfortable.medium_small}; + flex-direction: column; + align-items: flex-start; + gap: ${tokens.spacings.comfortable.medium_small}; + align-self: stretch; + border-radius: 5px; + background: ${tokens.colors.ui.background__info.rgba}; + `, + Message: styled(Typography)` + color: ${tokens.colors.text.static_icons__tertiary.rgba}; + `, +}; + +type InfoMessageProps = { + message: ReactNode; +}; + +export const InfoMessage = ({ message }: InfoMessageProps): JSX.Element => { + return ( + + + {message} + + + ); +}; + +export default InfoMessage; diff --git a/client/packages/components/src/components/service-now/components/NewIncident.tsx b/client/packages/components/src/components/service-now/components/NewIncident.tsx index 11edefcb5..1f808f0b7 100644 --- a/client/packages/components/src/components/service-now/components/NewIncident.tsx +++ b/client/packages/components/src/components/service-now/components/NewIncident.tsx @@ -15,6 +15,8 @@ import { UploadStatus } from '../types/types'; import { AttachmentsApiFailed } from './AttachmentsApiFailed'; import { AttachmentsPartialFail } from './AttachmentsPartialFail'; import { Inputs, inputSchema } from '../schema'; +import InfoMessage from './InfoMessage'; +import { tokens } from '@equinor/eds-tokens'; type NewIncidentProps = { onClose: () => void; @@ -35,6 +37,16 @@ const Style = { padding-top: 1rem; padding-bottom: 1rem; `, + ButtonWrapper: styled.div` + display: flex; + gap: 1rem; + `, +}; + +const formatDescription = (assistanceDescription: string, detailedDescription: string) => { + return ` + Type: Report an error\n\nWhat were you doing and what happened?\n${assistanceDescription}\n\nDescribe as detailed as possible:\n${detailedDescription} + `; }; export const NewIncident = ({ onClose }: NewIncidentProps) => { @@ -63,8 +75,12 @@ export const NewIncident = ({ onClose }: NewIncidentProps) => { const metadata = useIncidentMeta(); - const onSubmit: SubmitHandler = async ({ shortDescription, description, files }) => { - const incident = await createIncident({ shortDescription, description, metadata }); + const onSubmit: SubmitHandler = async ({ shortDescription, assistanceDescription, description, files }) => { + const incident = await createIncident({ + shortDescription, + description: formatDescription(assistanceDescription, description), + metadata, + }); const filesArray = Array.from(files) as File[]; if (filesArray.length > 0 && incident?.id) { @@ -110,7 +126,18 @@ export const NewIncident = ({ onClose }: NewIncidentProps) => { return ( - Report an Error + Report an Error + + Provide more details for faster, better, and more relevant support. +
+
+ Use this form if something is not working as expected or shows an error message. For general + assistance, please use the 'I need help' form. + + } + /> {Object.values(errors).length > 0 && ( { {createIncidentError && ( - - + + + + )} @@ -144,21 +173,52 @@ export const NewIncident = ({ onClose }: NewIncidentProps) => { variant={errors.shortDescription && 'error'} helperText={errors.shortDescription?.message} inputIcon={errors.shortDescription && } - label="Short Description" + label="Short Description*" + placeholder="Ticket title, please keep short and concise" maxLength={51} + required /> + + What were you doing and what happened? * +
+
+ Describe any error messages, unexpected behavior, and what you expected to happen. + + } + inputIcon={errors.assistanceDescription && } + multiline + rows={5} + required + /> + Describe as detailed as possible * +
+
+ What you clicked, where, any error messages, unexpected behavior, and what you expected to + happen. + + } inputIcon={errors.description && } multiline rows={5} + required /> - + { reset({ files }); }} /> - + + NB: Check that you capture the entire screen when uploading a screenshot + diff --git a/client/packages/components/src/components/service-now/hooks/use-incident-meta.ts b/client/packages/components/src/components/service-now/hooks/use-incident-meta.ts index 6b7c32ba7..8fa94d611 100644 --- a/client/packages/components/src/components/service-now/hooks/use-incident-meta.ts +++ b/client/packages/components/src/components/service-now/hooks/use-incident-meta.ts @@ -11,6 +11,7 @@ export const useIncidentMeta = () => { contextName: context?.title || 'unknown', contextType: context?.type.id || 'unknown', currentApp: currentApp?.appKey || 'portal', + currentAppCategory: currentApp?.manifest?.category?.displayName || 'unknown', userAgent: window.navigator.userAgent || 'unknown', }; }; diff --git a/client/packages/components/src/components/service-now/hooks/use-service-now-query.ts b/client/packages/components/src/components/service-now/hooks/use-service-now-query.ts index 7efdf4752..6a938ff9a 100644 --- a/client/packages/components/src/components/service-now/hooks/use-service-now-query.ts +++ b/client/packages/components/src/components/service-now/hooks/use-service-now-query.ts @@ -28,7 +28,11 @@ export const useCreateServiceNowIncidents = () => { return useMutation< Incident, FormattedError, - { description: string; shortDescription: string; metadata: Record }, + { + description: string; + shortDescription: string; + metadata: Record; + }, Incident >({ mutationKey: ['new-service-now-incidents', user?.localAccountId || ''], diff --git a/client/packages/components/src/components/service-now/query/service-now-query.ts b/client/packages/components/src/components/service-now/query/service-now-query.ts index 7aa2cc390..161f5a9ad 100644 --- a/client/packages/components/src/components/service-now/query/service-now-query.ts +++ b/client/packages/components/src/components/service-now/query/service-now-query.ts @@ -19,7 +19,11 @@ export const getIncidentsQuery = async (client: IHttpClient, azureUniqueId?: str export const createIncidentsQuery = async ( client: IHttpClient, - body: { shortDescription: string; description: string; metadata: Record }, + body: { + description: string; + shortDescription: string; + metadata: Record; + }, azureUniqueId?: string ) => { const response = await client.fetch(`persons/${azureUniqueId}/incidents`, { diff --git a/client/packages/components/src/components/service-now/schema/index.ts b/client/packages/components/src/components/service-now/schema/index.ts index 676780965..3596cff88 100644 --- a/client/packages/components/src/components/service-now/schema/index.ts +++ b/client/packages/components/src/components/service-now/schema/index.ts @@ -1,6 +1,21 @@ import * as z from 'zod'; export const inputSchema = z + .object({ + shortDescription: z.string().min(3, 'Short description must contain at least 3 character(s)').max(50), + assistanceDescription: z + .string() + .min(3, 'Assistance description must contain at least 3 character(s)') + .max(300, 'Assistance description can contain at most 300 character(s)'), + description: z + .string() + .min(3, 'Description must contain at least 3 character(s)') + .max(300, 'Description can contain at most 300 character(s)'), + files: z.any().nullable(), + }) + .required(); + +export const helpInput = z .object({ shortDescription: z.string().min(3, 'Short description must contain at least 3 character(s)').max(50), description: z @@ -12,3 +27,4 @@ export const inputSchema = z .required(); export type Inputs = z.infer; +export type HelpInput = z.infer; diff --git a/client/packages/components/src/components/service-now/types/types.ts b/client/packages/components/src/components/service-now/types/types.ts index e308a4bd3..f7731244a 100644 --- a/client/packages/components/src/components/service-now/types/types.ts +++ b/client/packages/components/src/components/service-now/types/types.ts @@ -13,6 +13,7 @@ export type Incident = { link: string; number: string; shortDescription: string; + assistanceDescription: string; state: string; type: string; }; diff --git a/client/packages/portal-client/src/lib/portal-framework-config.tsx b/client/packages/portal-client/src/lib/portal-framework-config.tsx index 6366f1739..c078708bf 100644 --- a/client/packages/portal-client/src/lib/portal-framework-config.tsx +++ b/client/packages/portal-client/src/lib/portal-framework-config.tsx @@ -9,13 +9,7 @@ import { skip } from 'rxjs'; import { replaceContextInPathname } from '../utils/context-utils'; import { enableAgGrid } from '@equinor/fusion-framework-module-ag-grid'; import { signalRConfigurator } from './signal-ir-configurator'; -import { - enablePortalApps, - enablePortalMenu, - enableTelemetry, - IPortalConfigProvider, - TelemetryModule, -} from '@portal/core'; +import { enablePortalApps, enablePortalMenu, enableTelemetry, TelemetryModule } from '@portal/core'; import { LoggerLevel, PortalConfig } from '@portal/types'; import { enableContext } from '@equinor/fusion-framework-module-context'; import { enableFeatureFlagging } from '@equinor/fusion-framework-module-feature-flag';