diff --git a/assets/service-catalog-bundle.js b/assets/service-catalog-bundle.js index 263fa49ee..f468bfbed 100644 --- a/assets/service-catalog-bundle.js +++ b/assets/service-catalog-bundle.js @@ -181,11 +181,16 @@ const formatField = (field) => { error: null, }; }; -const isAssociatedLookupField = (field) => { +const isAssociatedLookupField = (field, setAssociatedLookupField) => { const customObjectKey = getCustomObjectKey(field.relationship_target_type); - return customObjectKey === "service_catalog_item"; + if (customObjectKey === "service_catalog_item") { + setAssociatedLookupField(formatField(field)); + return true; + } + else + return false; }; -const fetchTicketFields = async (form_id, baseLocale) => { +const fetchTicketFields = async (form_id, baseLocale, setAssociatedLookupField) => { try { const [formResponse, fieldsResponse] = await Promise.all([ fetch(`/api/v2/ticket_forms/${form_id}`), @@ -206,7 +211,7 @@ const fetchTicketFields = async (form_id, baseLocale) => { const ticketField = ticketFieldsData.find((field) => field.id === id); if (ticketField && !(ticketField.type === "lookup" && - isAssociatedLookupField(ticketField)) && + isAssociatedLookupField(ticketField, setAssociatedLookupField)) && ticketField.editable_in_portal) { return formatField(ticketField); } @@ -222,11 +227,12 @@ const fetchTicketFields = async (form_id, baseLocale) => { }; function useItemFormFields(serviceCatalogItem, baseLocale) { const [requestFields, setRequestFields] = reactExports.useState([]); + const [associatedLookupField, setAssociatedLookupField] = reactExports.useState(); reactExports.useEffect(() => { const fetchAndSetFields = async () => { if (serviceCatalogItem && serviceCatalogItem.form_id) { try { - await fetchTicketFields(serviceCatalogItem.form_id, baseLocale).then((ticketFields) => setRequestFields(ticketFields)); + await fetchTicketFields(serviceCatalogItem.form_id, baseLocale, setAssociatedLookupField).then((ticketFields) => setRequestFields(ticketFields)); } catch (error) { console.error("Error fetching ticket fields:", error); @@ -240,7 +246,12 @@ function useItemFormFields(serviceCatalogItem, baseLocale) { ? { ...ticketField, value } : ticketField)); }, [requestFields]); - return { requestFields, handleChange }; + return { + requestFields, + associatedLookupField, + setRequestFields, + handleChange, + }; } const DescriptionWrapper = styled.div ` @@ -360,7 +371,7 @@ const LeftColumn = styled.div ` `; function ItemRequestForm({ requestFields, serviceCatalogItem, baseLocale, hasAtMentions, userRole, userId, brandId, defaultOrganizationId, handleChange, onSubmit, }) { const { t } = useTranslation(); - return (jsxRuntimeExports.jsxs(Form, { onSubmit: onSubmit, children: [jsxRuntimeExports.jsxs(LeftColumn, { children: [jsxRuntimeExports.jsx(CollapsibleDescription, { title: serviceCatalogItem.name, description: serviceCatalogItem.description }), jsxRuntimeExports.jsx(FieldsContainer, { children: requestFields.map((field) => (jsxRuntimeExports.jsx(TicketField, { field: field, baseLocale: baseLocale, hasAtMentions: hasAtMentions, userRole: userRole, userId: userId, brandId: brandId, defaultOrganizationId: defaultOrganizationId, handleChange: handleChange }, field.id))) })] }), jsxRuntimeExports.jsx(RightColumn, { children: jsxRuntimeExports.jsx(ButtonWrapper, { children: jsxRuntimeExports.jsx(Button, { isPrimary: true, size: "large", isStretched: true, type: "submit", children: t("service-catalog.item.submit-button", "Submit request") }) }) })] })); + return (jsxRuntimeExports.jsxs(Form, { onSubmit: onSubmit, noValidate: true, children: [jsxRuntimeExports.jsxs(LeftColumn, { children: [jsxRuntimeExports.jsx(CollapsibleDescription, { title: serviceCatalogItem.name, description: serviceCatalogItem.description }), jsxRuntimeExports.jsx(FieldsContainer, { children: requestFields.map((field) => (jsxRuntimeExports.jsx(TicketField, { field: field, baseLocale: baseLocale, hasAtMentions: hasAtMentions, userRole: userRole, userId: userId, brandId: brandId, defaultOrganizationId: defaultOrganizationId, handleChange: handleChange }, field.id))) })] }), jsxRuntimeExports.jsx(RightColumn, { children: jsxRuntimeExports.jsx(ButtonWrapper, { children: jsxRuntimeExports.jsx(Button, { isPrimary: true, size: "large", isStretched: true, type: "submit", children: t("service-catalog.item.submit-button", "Submit request") }) }) })] })); } function useServiceCatalogItem(serviceItemId) { @@ -386,14 +397,23 @@ function useServiceCatalogItem(serviceItemId) { return serviceCatalogItem; } -function useServiceFormSubmit(serviceCatalogItem, requestFields) { +function useServiceFormSubmit(serviceCatalogItem, requestFields, associatedLookupField, baseLocale) { const submitServiceItemRequest = async () => { if (!serviceCatalogItem) { return; } - console.log("requestFields", requestFields); const currentUserRequest = await fetch("/api/v2/users/me.json"); const currentUser = await currentUserRequest.json(); + const customFields = requestFields.map((field) => { + if (field.type !== "subject" && field.type !== "description") { + return { + id: field.id, + value: field.value, + }; + } + else + return; + }); const response = await fetch("/api/v2/requests", { method: "POST", headers: { @@ -407,22 +427,19 @@ function useServiceFormSubmit(serviceCatalogItem, requestFields) { body: serviceCatalogItem.description, }, ticket_form_id: serviceCatalogItem.form_id, - custom_fields: requestFields.map((field) => { - if (field.type !== "subject" && field.type !== "description") { - return { - id: field.id, - value: field.value, - }; - } - else - return; - }), + custom_fields: [ + ...customFields, + { id: associatedLookupField?.id, value: serviceCatalogItem.id }, + ], + via: { + channel: "web form", + source: 50, + }, + locale: baseLocale, }, }), }); - const data = await response.json(); - const redirectUrl = "/hc/requests/" + data.request.id; - window.location.href = redirectUrl; + return response; }; return { submitServiceItemRequest }; } @@ -439,21 +456,34 @@ const Container = styled.div ` `; function ServiceCatalogItemPage({ serviceCatalogItemId, baseLocale, hasAtMentions, userRole, organizations, userId, brandId, }) { const serviceCatalogItem = useServiceCatalogItem(serviceCatalogItemId); - const { requestFields, handleChange } = useItemFormFields(serviceCatalogItem, baseLocale); - const { submitServiceItemRequest } = useServiceFormSubmit(serviceCatalogItem, requestFields); + const { requestFields, associatedLookupField, setRequestFields, handleChange, } = useItemFormFields(serviceCatalogItem, baseLocale); + const { submitServiceItemRequest } = useServiceFormSubmit(serviceCatalogItem, requestFields, associatedLookupField, baseLocale); const handleRequestSubmit = async (e) => { e.preventDefault(); - try { - await submitServiceItemRequest(); + const response = await submitServiceItemRequest(); + if (response && !response.ok) { + const errorData = await response.json(); + if (errorData.error === "RecordInvalid") { + const invalidFieldErros = errorData.details.base; + const updatedFields = requestFields.map((field) => { + const errorField = invalidFieldErros.find((errorField) => errorField.field_key === field.id); + return errorField + ? { ...field, error: errorField.description } + : field; + }); + setRequestFields(updatedFields); + } } - catch (error) { - console.error("Error submitting service item request:", error); + else if (response && response.ok) { + const data = await response?.json(); + const redirectUrl = "/hc/requests/" + data.request.id; + window.location.href = redirectUrl; } }; const defaultOrganizationId = organizations.length > 0 && organizations[0]?.id ? organizations[0]?.id?.toString() : null; - return (jsxRuntimeExports.jsx(Container, { children: serviceCatalogItem && (jsxRuntimeExports.jsx(ItemRequestForm, { requestFields: requestFields, serviceCatalogItem: serviceCatalogItem, baseLocale: baseLocale, hasAtMentions: hasAtMentions, userRole: userRole, userId: userId, brandId: brandId, defaultOrganizationId: defaultOrganizationId, handleChange: handleChange, onSubmit: handleRequestSubmit })) })); + return serviceCatalogItem ? (jsxRuntimeExports.jsx(Container, { children: serviceCatalogItem && (jsxRuntimeExports.jsx(ItemRequestForm, { requestFields: requestFields, serviceCatalogItem: serviceCatalogItem, baseLocale: baseLocale, hasAtMentions: hasAtMentions, userRole: userRole, userId: userId, brandId: brandId, defaultOrganizationId: defaultOrganizationId, handleChange: handleChange, onSubmit: handleRequestSubmit })) })) : null; } async function renderServiceCatalogItem(container, settings, props) { diff --git a/src/modules/service-catalog/ServiceCatalogItemPage.tsx b/src/modules/service-catalog/ServiceCatalogItemPage.tsx index 58444561d..4ac15d031 100644 --- a/src/modules/service-catalog/ServiceCatalogItemPage.tsx +++ b/src/modules/service-catalog/ServiceCatalogItemPage.tsx @@ -4,6 +4,7 @@ import { ItemRequestForm } from "./components/service-catalog-item/ItemRequestFo import type { Organization } from "../ticket-fields"; import { useServiceCatalogItem } from "./useServiceCatalogItem"; import { useServiceFormSubmit } from "./useServiceFormSubmit"; +import type { RequestError } from "./data-types/RequestError"; const Container = styled.div` display: flex; @@ -37,21 +38,41 @@ export function ServiceCatalogItemPage({ brandId, }: ServiceCatalogItemPageProps) { const serviceCatalogItem = useServiceCatalogItem(serviceCatalogItemId); - const { requestFields, handleChange } = useItemFormFields( - serviceCatalogItem, - baseLocale - ); + const { + requestFields, + associatedLookupField, + setRequestFields, + handleChange, + } = useItemFormFields(serviceCatalogItem, baseLocale); const { submitServiceItemRequest } = useServiceFormSubmit( serviceCatalogItem, - requestFields + requestFields, + associatedLookupField, + baseLocale ); const handleRequestSubmit = async (e: React.FormEvent) => { e.preventDefault(); - try { - await submitServiceItemRequest(); - } catch (error) { - console.error("Error submitting service item request:", error); + + const response = await submitServiceItemRequest(); + if (response && !response.ok) { + const errorData = await response.json(); + if (errorData.error === "RecordInvalid") { + const invalidFieldErros = errorData.details.base; + const updatedFields = requestFields.map((field) => { + const errorField = invalidFieldErros.find( + (errorField: RequestError) => errorField.field_key === field.id + ); + return errorField + ? { ...field, error: errorField.description } + : field; + }); + setRequestFields(updatedFields); + } + } else if (response && response.ok) { + const data = await response?.json(); + const redirectUrl = "/hc/requests/" + data.request.id; + window.location.href = redirectUrl; } }; diff --git a/src/modules/service-catalog/components/service-catalog-item/ItemRequestForm.tsx b/src/modules/service-catalog/components/service-catalog-item/ItemRequestForm.tsx index 7ac6fb0bd..01715a40c 100644 --- a/src/modules/service-catalog/components/service-catalog-item/ItemRequestForm.tsx +++ b/src/modules/service-catalog/components/service-catalog-item/ItemRequestForm.tsx @@ -110,7 +110,7 @@ export function ItemRequestForm({ }: ItemRequestFormProps) { const { t } = useTranslation(); return ( -
+ { }; }; -const isAssociatedLookupField = (field: TicketField) => { +const isAssociatedLookupField = ( + field: TicketField, + setAssociatedLookupField: (field: Field | null) => void +) => { const customObjectKey = getCustomObjectKey( field.relationship_target_type as string ); - return customObjectKey === "service_catalog_item"; + if (customObjectKey === "service_catalog_item") { + setAssociatedLookupField(formatField(field)); + return true; + } else return false; }; const fetchTicketFields = async ( form_id: string, - baseLocale: string + baseLocale: string, + setAssociatedLookupField: (field: Field | null) => void ): Promise => { try { const [formResponse, fieldsResponse] = await Promise.all([ @@ -66,7 +73,7 @@ const fetchTicketFields = async ( ticketField && !( ticketField.type === "lookup" && - isAssociatedLookupField(ticketField) + isAssociatedLookupField(ticketField, setAssociatedLookupField) ) && ticketField.editable_in_portal ) { @@ -87,14 +94,18 @@ export function useItemFormFields( baseLocale: string ) { const [requestFields, setRequestFields] = useState([]); + const [associatedLookupField, setAssociatedLookupField] = + useState(); useEffect(() => { const fetchAndSetFields = async () => { if (serviceCatalogItem && serviceCatalogItem.form_id) { try { - await fetchTicketFields(serviceCatalogItem.form_id, baseLocale).then( - (ticketFields) => setRequestFields(ticketFields) - ); + await fetchTicketFields( + serviceCatalogItem.form_id, + baseLocale, + setAssociatedLookupField + ).then((ticketFields) => setRequestFields(ticketFields)); } catch (error) { console.error("Error fetching ticket fields:", error); } @@ -117,5 +128,10 @@ export function useItemFormFields( [requestFields] ); - return { requestFields, handleChange }; + return { + requestFields, + associatedLookupField, + setRequestFields, + handleChange, + }; } diff --git a/src/modules/service-catalog/data-types/RequestError.ts b/src/modules/service-catalog/data-types/RequestError.ts new file mode 100644 index 000000000..cc5600c6c --- /dev/null +++ b/src/modules/service-catalog/data-types/RequestError.ts @@ -0,0 +1,5 @@ +export interface RequestError { + description: string; + error: string; + field_key: number; +} diff --git a/src/modules/service-catalog/data-types/TicketField.ts b/src/modules/service-catalog/data-types/TicketField.ts index 22837f452..8345b916e 100644 --- a/src/modules/service-catalog/data-types/TicketField.ts +++ b/src/modules/service-catalog/data-types/TicketField.ts @@ -5,6 +5,7 @@ export interface TicketField { title: string; value?: string | string[] | boolean; required: boolean; + error: string | null; description: string; type: string; custom_field_options: FieldOption[]; diff --git a/src/modules/service-catalog/useServiceFormSubmit.tsx b/src/modules/service-catalog/useServiceFormSubmit.tsx index a11013378..008037598 100644 --- a/src/modules/service-catalog/useServiceFormSubmit.tsx +++ b/src/modules/service-catalog/useServiceFormSubmit.tsx @@ -3,15 +3,24 @@ import type { ServiceCatalogItem } from "./data-types/ServiceCatalogItem"; export function useServiceFormSubmit( serviceCatalogItem: ServiceCatalogItem | undefined, - requestFields: Field[] + requestFields: Field[], + associatedLookupField: Field | null | undefined, + baseLocale: string ) { const submitServiceItemRequest = async () => { if (!serviceCatalogItem) { return; } - console.log("requestFields", requestFields); const currentUserRequest = await fetch("/api/v2/users/me.json"); const currentUser = await currentUserRequest.json(); + const customFields = requestFields.map((field) => { + if (field.type !== "subject" && field.type !== "description") { + return { + id: field.id, + value: field.value, + }; + } else return; + }); const response = await fetch("/api/v2/requests", { method: "POST", headers: { @@ -25,20 +34,19 @@ export function useServiceFormSubmit( body: serviceCatalogItem.description, }, ticket_form_id: serviceCatalogItem.form_id, - custom_fields: requestFields.map((field) => { - if (field.type !== "subject" && field.type !== "description") { - return { - id: field.id, - value: field.value, - }; - } else return; - }), + custom_fields: [ + ...customFields, + { id: associatedLookupField?.id, value: serviceCatalogItem.id }, + ], + via: { + channel: "web form", + source: 50, + }, + locale: baseLocale, }, }), }); - const data = await response.json(); - const redirectUrl = "/hc/requests/" + data.request.id; - window.location.href = redirectUrl; + return response; }; return { submitServiceItemRequest };