diff --git a/frontend/package.json b/frontend/package.json index a9c5a698858..b7199e50727 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -94,7 +94,7 @@ "react-google-recaptcha": "2.1.0", "react-gtm-module": "2.0.11", "react-highlight-words": "0.17.0", - "react-hook-form": "7.12.2", + "react-hook-form": "7.61.1", "react-intl": "5.21.0", "react-is": "17.0.2", "react-konva": "17.0.2-5", diff --git a/frontend/src/v5/store/drawings/revisions/drawingRevisions.types.ts b/frontend/src/v5/store/drawings/revisions/drawingRevisions.types.ts index 246fa69334e..a372c65ba7c 100644 --- a/frontend/src/v5/store/drawings/revisions/drawingRevisions.types.ts +++ b/frontend/src/v5/store/drawings/revisions/drawingRevisions.types.ts @@ -74,3 +74,5 @@ export type UploadItemFields = CreateDrawingRevisionBody & { extension: string; isMultiPagePdf?: boolean; }; + +export type UploadDrawingFormType = { uploads: UploadItemFields[] }; \ No newline at end of file diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx index 06878a1332a..a4b57c3e0a1 100644 --- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx +++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfileModal.component.tsx @@ -58,7 +58,7 @@ export const EditProfileModal = ({ open, onClickClose, initialTab }: EditProfile const defaultPersonalValues = defaults( pick(omitBy(user, isNull), ['firstName', 'lastName', 'email', 'company', 'countryCode']), - { company: '', avatarFile: '' }, + { company: '', avatarFile: undefined }, ); const onTabChange = (_, selectedTab) => setActiveTab(selectedTab); diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfileAvatar/editProfileAvatar.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfileAvatar/editProfileAvatar.component.tsx index b29017e33f1..c2fc8174a8a 100644 --- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfileAvatar/editProfileAvatar.component.tsx +++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfileAvatar/editProfileAvatar.component.tsx @@ -110,7 +110,7 @@ export const EditProfileAvatar = ({ user }: EditProfilePersonalTabProps) => { /> - {(fileSizeError || error) && } + {(fileSizeError || error) && } ); }; diff --git a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx index e8c4b027bb0..45b8303795a 100644 --- a/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx +++ b/frontend/src/v5/ui/components/shared/userMenu/editProfileModal/editProfilePersonalTab/editProfilePersonalTab.component.tsx @@ -61,7 +61,7 @@ export const EditProfilePersonalTab = ({ reset, setError: setFormError, control, - formState: { errors: formErrors, isDirty, touchedFields, isSubmitting }, + formState: { errors: formErrors, isSubmitting }, } = useFormContext(); const userIsMissingRequiredData = userHasMissingRequiredData(user); @@ -107,10 +107,9 @@ export const EditProfilePersonalTab = ({ const canSubmit = isEmpty(formErrors) && fieldsAreDirty; useEffect(() => { - if (submitWasSuccessful) { - setSubmitWasSuccessful(false); - } - }, [JSON.stringify(isDirty), touchedFields]); + if (!fieldsAreDirty || !submitWasSuccessful) return + setSubmitWasSuccessful(false); + }, [fieldsAreDirty]); useEffect(() => { if (userIsMissingRequiredData) { diff --git a/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/createDrawingDialog.component.tsx b/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/createDrawingDialog.component.tsx index 02cf188bbdd..4803ca4796d 100644 --- a/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/createDrawingDialog.component.tsx +++ b/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/createDrawingDialog.component.tsx @@ -49,7 +49,11 @@ export const CreateDrawingDialog = ({ open, onClickClose }) => { onSubmit={handleSubmit(onSubmit)} confirmLabel={formatMessage({ id: 'drawings.creation.ok', defaultMessage: 'Create Drawing' })} maxWidth="sm" - {...formState} + // {...formState} Some properties in formState where + // converted from enumerable to non-enumerable isValid one of them so {...formstate} will no longer + // pass the correct values. + isValid={formState.isValid} + isSubmitting={formState.isSubmitting} > diff --git a/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/editDrawingDialog.component.tsx b/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/editDrawingDialog.component.tsx index 2a0706a60d4..35f7044a9d3 100644 --- a/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/editDrawingDialog.component.tsx +++ b/frontend/src/v5/ui/routes/dashboard/projects/drawings/drawingDialogs/editDrawingDialog.component.tsx @@ -72,7 +72,7 @@ export const EditDrawingDialog = ({ open, onClickClose, drawingId }:Props) => { onSubmit={handleSubmit(onSubmit)} confirmLabel={formatMessage({ id: 'drawings.edit.ok', defaultMessage: 'Save Drawing' })} maxWidth="sm" - {...formState} + isSubmitting={formState.isSubmitting} isValid={dirtyValuesChanged(formData, drawingId) && formState.isValid} > {drawing.calibration.units diff --git a/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/sidebarForm/sidebarForm.component.tsx b/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/sidebarForm/sidebarForm.component.tsx index 9f8fe7baff2..a18817b177c 100644 --- a/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/sidebarForm/sidebarForm.component.tsx +++ b/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/sidebarForm/sidebarForm.component.tsx @@ -17,7 +17,7 @@ import { formatMessage } from '@/v5/services/intl'; import { FormattedMessage } from 'react-intl'; -import { useFormContext } from 'react-hook-form'; +import { FieldError, useFormContext } from 'react-hook-form'; import { MenuItem } from '@mui/material'; import { FormNumberField, FormSelect, FormTextField } from '@controls/inputs/formInputs.component'; import { get, isNumber } from 'lodash'; @@ -30,16 +30,17 @@ import { DoubleInputLineContainer } from '../../drawingDialogs/drawingForm.style import { Loader } from '@/v4/routes/components/loader/loader.component'; import { DrawingRevisionsActionsDispatchers, DrawingsActionsDispatchers } from '@/v5/services/actionsDispatchers'; import { CALIBRATION_INVALID_RANGE_ERROR } from '@/v5/validation/drawingSchemes/drawingSchemes'; +import { UploadDrawingFormType } from '@/v5/store/drawings/revisions/drawingRevisions.types'; export const SidebarForm = () => { const teamspace = TeamspacesHooksSelectors.selectCurrentTeamspace(); const project = ProjectsHooksSelectors.selectCurrentProject(); const types = DrawingsHooksSelectors.selectTypes(); - const { getValues, formState: { errors, dirtyFields }, trigger, watch } = useFormContext(); + const { getValues, formState: { errors, dirtyFields }, trigger, watch } = useFormContext(); const { fields, selectedId } = useContext(UploadFilesContext); // @ts-ignore - const selectedIndex = fields.findIndex(({ uploadId }) => uploadId === selectedId); - const revisionPrefix = `uploads.${selectedIndex}`; + const selectedIndex: number = fields.findIndex(({ uploadId }) => uploadId === selectedId); + const revisionPrefix = `uploads.${selectedIndex}` as `uploads.${number}`; const [drawingId, drawingName] = getValues([`${revisionPrefix}.drawingId`, `${revisionPrefix}.drawingName`]); const disableDrawingFields = !(drawingName && !drawingId); const getError = (field: string) => get(errors, `${revisionPrefix}.${field}`); @@ -48,7 +49,8 @@ export const SidebarForm = () => { const hasPendingRevisions = !!DrawingRevisionsHooksSelectors.selectRevisionsPending(drawingId); const drawingRevisionsArePending = !!DrawingRevisionsHooksSelectors.selectIsPending(drawingId); const needsFetchingCalibration = hasActiveRevisions && (hasPendingRevisions || drawingRevisionsArePending) && !isNumber(verticalRange[0]); - const hideBottomExtentError = (errors.calibration?.verticalRange || []).some((e) => e.message === CALIBRATION_INVALID_RANGE_ERROR); + const hideBottomExtentError = (errors.uploads?.[selectedIndex]?.calibration?.verticalRange || []) + .some((e: FieldError) => e.message === CALIBRATION_INVALID_RANGE_ERROR); useEffect(() => { if (get(dirtyFields, `${revisionPrefix}.calibration.verticalRange`)?.some((v) => v)) { diff --git a/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadDrawingRevisionForm.component.tsx b/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadDrawingRevisionForm.component.tsx index cb9e559df99..e75cfef7397 100644 --- a/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadDrawingRevisionForm.component.tsx +++ b/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadDrawingRevisionForm.component.tsx @@ -29,14 +29,14 @@ import { } from '@/v5/services/selectorsHooks'; import { getSupportedDrawingRevisionsFileExtensions } from '@controls/fileUploader/uploadFile'; import { UploadFiles } from '@components/shared/uploadFiles/uploadFiles.component'; -import { UploadFieldArray, UploadFilesContextComponent } from '@components/shared/uploadFiles/uploadFilesContext'; +import { UploadFilesContextComponent } from '@components/shared/uploadFiles/uploadFilesContext'; import { SidebarForm } from './sidebarForm/sidebarForm.component'; import { IDrawing } from '@/v5/store/drawings/drawings.types'; import { DrawingRevisionsActionsDispatchers, DrawingsActionsDispatchers } from '@/v5/services/actionsDispatchers'; import { UploadList } from './uploadList/uploadList.component'; import { parseFileName, reduceFileData, isPdf, getPdfFirstPage, fileToPdf, pdfToFile } from '@components/shared/uploadFiles/uploadFiles.helpers'; -import { UploadItemFields } from '@/v5/store/drawings/revisions/drawingRevisions.types'; import { formatInfoUnit } from '@/v5/helpers/intl.helper'; +import { UploadDrawingFormType } from '@/v5/store/drawings/revisions/drawingRevisions.types'; const REVISION_NAME_MAX_LENGTH = 50; @@ -44,8 +44,6 @@ type UploadModalLabelTypes = { isUploading: boolean; fileCount: number; }; -type FormType = UploadFieldArray; - const uploadModalLabels = ({ isUploading, fileCount }: UploadModalLabelTypes) => (isUploading ? { title: formatMessage({ @@ -94,7 +92,7 @@ export const UploadDrawingRevisionForm = ({ const [isPresetLoading, setIsPresetLoading] = useState(!!presetFile); const [isUploading, setIsUploading] = useState(false); - const formData = useForm({ + const formData = useForm({ mode: 'onChange', resolver: !isUploading ? yupResolver(UploadsSchema) : undefined, context: { diff --git a/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadList/uploadListItem/uploadListItem.component.tsx b/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadList/uploadListItem/uploadListItem.component.tsx index 13dea8f667c..027c90573cd 100644 --- a/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadList/uploadListItem/uploadListItem.component.tsx +++ b/frontend/src/v5/ui/routes/dashboard/projects/drawings/uploadDrawingRevisionForm/uploadList/uploadListItem/uploadListItem.component.tsx @@ -137,9 +137,12 @@ export const UploadListItem = ({ }, [drawingId, revCodeError]); useEffect(() => { - setValue(revisionPrefix, sanitiseDrawing(selectedDrawing)); - }, [JSON.stringify(selectedDrawing)]); + for (const [key, val] of Object.entries(sanitiseDrawing(selectedDrawing))) { + setValue(`${revisionPrefix}.${key}`, val); + } + }, [JSON.stringify(selectedDrawing)]); + useEffect(() => { if (selectedDrawing?._id) { if (selectRevisionsPending(getState(), selectedDrawing._id)) { diff --git a/frontend/src/v5/ui/routes/viewer/tickets/newTicket/newTicket.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/newTicket/newTicket.component.tsx index 0ac81781b6c..1086de07d05 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/newTicket/newTicket.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/newTicket/newTicket.component.tsx @@ -57,7 +57,7 @@ export const NewTicketCard = () => { const formData = useForm({ resolver: yupResolver(isLoading ? null : getTicketValidator(template)), - mode: 'onChange', + mode: 'all', defaultValues: defaultTicket, }); @@ -92,7 +92,7 @@ export const NewTicketCard = () => { await promiseToResolve; }; - const updateUnsavedTicket = () => TicketsCardActionsDispatchers.setUnsavedTicket(formData.getValues()); + const updateUnsavedTicket = () => TicketsCardActionsDispatchers.setUnsavedTicket(cloneDeep(formData.getValues())); useEffect(() => { if (!templateAlreadyFetched(template)) { @@ -162,7 +162,6 @@ export const NewTicketCard = () => { // Im not sure this is still needed here, because we are already depending on react-hook-form to fill the form ticket={defaultTicket} focusOnTitle - onPropertyBlur={updateUnsavedTicket} /> )} diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/commentsPanel/commentBox/commentBox.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/commentsPanel/commentBox/commentBox.component.tsx index f1389910ce2..0c91537dd8a 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/commentsPanel/commentBox/commentBox.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/commentsPanel/commentBox/commentBox.component.tsx @@ -100,7 +100,7 @@ export const CommentBox = ({ commentId, onCancelEdit, message = '', images = [], return { ...image, src: getTicketResourceUrl(teamspace, project, containerOrFederation, ticketId, image.id, isFederation) }; }); - const { watch, reset, control } = useForm<{ message: string, images: File[] }>({ + const { watch, reset, control } = useForm<{ message: string, images: string[] }>({ mode: 'all', defaultValues: { message: desanitiseMessage(message), diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/properties.hooks.ts b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/properties.hooks.ts new file mode 100644 index 00000000000..83aff83288c --- /dev/null +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/properties.hooks.ts @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2025 3D Repo Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { useRef, useEffect } from "react"; + +export const useChangeAndBlur = (value, onChange, onBlur) => { + const shouldBlur = useRef(false); + + useEffect(() => { + if (!shouldBlur.current) return; + shouldBlur.current = false; + onBlur?.() + }, [value, onBlur]); + + return (val) => { + shouldBlur.current = true; + onChange?.(val); + } +}; \ No newline at end of file diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageContent/ticketImage/ticketImage.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageContent/ticketImage/ticketImage.component.tsx index 57e665eba6c..ef17e2df7b5 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageContent/ticketImage/ticketImage.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageContent/ticketImage/ticketImage.component.tsx @@ -26,33 +26,34 @@ import { InputContainer, Label } from './ticketImage.styles'; import { ImagesModal } from '@components/shared/modalsDispatcher/templates/imagesModal/imagesModal.component'; import { DialogsActionsDispatchers } from '@/v5/services/actionsDispatchers'; import { useSyncProps } from '@/v5/helpers/syncProps.hooks'; +import { useChangeAndBlur as useChangeAndBlur } from '../../properties.hooks'; type TicketImageProps = FormInputProps & { onImageClick: () => void; inputRef? }; export const TicketImage = ({ value, onChange, onBlur, disabled, label, helperText, onImageClick, inputRef = undefined, ...props }: TicketImageProps) => { const imgSrc = getImgSrc(value); const [imgInModal, setImgInModal] = useState(imgSrc); const syncProps = useSyncProps({ images: [imgInModal] }); + const onChangeAndBlur = useChangeAndBlur(value, onChange, onBlur); const handleImageClick = () => DialogsActionsDispatchers.open(ImagesModal, { onAddMarkup: disabled ? null - : (newValue) => onChange(newValue ? stripBase64Prefix(newValue) : null), + : (newValue) => onChangeAndBlur(newValue ? stripBase64Prefix(newValue) : null), }, syncProps); const onUploadNewImage = (newValue) => { if (!newValue) { - onChange(newValue); + onChangeAndBlur(newValue); return; } setImgInModal(newValue); DialogsActionsDispatchers.open(ImagesModal, { - onClose: (newImages) => onChange(stripBase64Prefix(newImages[0])), + onClose: (newImages) => onChangeAndBlur(stripBase64Prefix(newImages[0])), onAddMarkup: setImgInModal, }, syncProps); }; - useEffect(() => onBlur?.(), [value]); useEffect(() => { setImgInModal(imgSrc); }, [imgSrc]); return ( diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageList/ticketImageList.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageList/ticketImageList.component.tsx index 6eab2db22ec..07dfec6f53f 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageList/ticketImageList.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketImageList/ticketImageList.component.tsx @@ -41,6 +41,7 @@ import EmptyImageIcon from '@assets/icons/outlined/add_image_thin-outlined.svg'; import { uploadImages } from '@controls/fileUploader/uploadImages'; import { DrawingViewerService } from '@components/viewer/drawingViewer/drawingViewer.service'; import { ViewerCanvasesContext } from '../../../../viewerCanvases.context'; +import { useChangeAndBlur } from '../properties.hooks'; const EmptyImage = ({ disabled, onClick }) => ( @@ -72,9 +73,10 @@ export const TicketImageList = ({ value, onChange, onBlur, disabled, label, help const isProjectAdmin = ProjectsHooksSelectors.selectIsProjectAdmin(); const imgsSrcs = (value || []).map((img) => getImgSrc(img, imgContext)); const [imgsInModal, setImgsInModal] = useState(imgsSrcs); - - const onClose = () => onChange(imgsInModal?.length ? imgsInModal.map(getImgIdFromSrc) : null); - const onDeleteImages = () => onChange(null); + const onChangeAndBlur = useChangeAndBlur(value, onChange, onBlur); + + const onClose = () => onChangeAndBlur(imgsInModal?.length ? imgsInModal.map(getImgIdFromSrc) : null); + const onDeleteImages = () => onChangeAndBlur(null); const onDeleteImage = (index) => setImgsInModal(imgsInModal.filter((img, i) => index !== i)); const onEditImage = (newValue, index) => { if (newValue) { @@ -108,15 +110,10 @@ export const TicketImageList = ({ value, onChange, onBlur, disabled, label, help setImgsInModal(imgsInModal.concat(screenshot)); openImagesModal(displayImageIndex); }; + const upload3DScreenshot = async () => uploadScreenshot(await ViewerService.getScreenshot()); const upload2DScreenshot = async () => uploadScreenshot(await DrawingViewerService.getScreenshot()); - - useEffect(() => { - setImgsInModal(imgsSrcs); - onBlur?.(); - }, [value]); - return ( diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketView/ticketView.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketView/ticketView.component.tsx index fcd8ae19436..3f412f7c2d0 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketView/ticketView.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/properties/ticketView/ticketView.component.tsx @@ -19,7 +19,7 @@ import { Viewer as ViewerService } from '@/v4/services/viewer/viewer'; import ViewpointIcon from '@assets/icons/outlined/aim-outlined.svg'; import TickIcon from '@assets/icons/outlined/tick-outlined.svg'; import { stripBase64Prefix } from '@controls/fileUploader/imageFile.helper'; -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { cloneDeep, isEmpty } from 'lodash'; import { getImgSrc } from '@/v5/store/tickets/tickets.helpers'; @@ -40,6 +40,7 @@ import { GroupsActionMenu } from './viewActionMenu/menus/groupsActionMenu.compon import { ViewerInputContainer } from '../viewerInputContainer/viewerInputContainer.component'; import { useSyncProps } from '@/v5/helpers/syncProps.hooks'; import { DialogsActionsDispatchers } from '@/v5/services/actionsDispatchers'; +import { useChangeAndBlur } from '../properties.hooks'; type ITicketView = { value: Viewpoint | undefined; @@ -67,12 +68,13 @@ export const TicketView = ({ const imgSrc = getImgSrc(value?.screenshot); const [imgInModal, setImgInModal] = useState(imgSrc); const syncProps = useSyncProps({ images: [imgInModal] }); + const onChangeAndBlur = useChangeAndBlur(value, onChange, onBlur); // Image const handleImageClick = () => DialogsActionsDispatchers.open(ImagesModal, { onAddMarkup: disabled ? null - : (newValue) => onChange({ ...value, screenshot: stripBase64Prefix(newValue) }), + : (newValue) => onChangeAndBlur({ ...value, screenshot: stripBase64Prefix(newValue) }), }, syncProps); const handleNewImageUpload = (newImage, onSave) => { @@ -85,11 +87,11 @@ export const TicketView = ({ const onUpdateImage = (newValue) => { if (!newValue) { - onChange({ ...value, screenshot: null }); + onChangeAndBlur({ ...value, screenshot: null }); return; } - handleNewImageUpload(newValue, (newImage) => onChange({ ...value, screenshot: newImage })); + handleNewImageUpload(newValue, (newImage) => onChangeAndBlur({ ...value, screenshot: newImage })); }; // Viewpoint @@ -98,18 +100,18 @@ export const TicketView = ({ const screenshot = await ViewerService.getScreenshot(); const state = await getViewerState(); - handleNewImageUpload(screenshot, (newImage) => onChange({ screenshot: newImage, ...currentCameraAndClipping, state })); + handleNewImageUpload(screenshot, (newImage) => onChangeAndBlur({ screenshot: newImage, ...currentCameraAndClipping, state })); }; // Camera const onUpdateCamera = async () => { const currentCameraAndClipping = await ViewerService.getViewpoint(); - onChange?.({ ...value, ...currentCameraAndClipping }); + onChangeAndBlur?.({ ...value, ...currentCameraAndClipping }); }; const onDeleteCamera = async () => { const { camera, ...view } = value || {}; - onChange?.(isEmpty(view) ? null : view); + onChangeAndBlur?.(isEmpty(view) ? null : view); }; const onGoToCamera = async () => { @@ -126,10 +128,9 @@ export const TicketView = ({ state.colored = []; state.hidden = []; state.transformed = []; - onChange?.({ state, ...view }); + onChangeAndBlur?.({ state, ...view }); }; - useEffect(() => onBlur?.(), [value]); useEffect(() => { setImgInModal(imgSrc); }, [imgSrc]); const onGroupsClick = () => { diff --git a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/ticketGroups/groups/groupActionMenu/groupSettingsForm/groupSettingsForm.component.tsx b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/ticketGroups/groups/groupActionMenu/groupSettingsForm/groupSettingsForm.component.tsx index 6c9cb3ec2f4..7e38e269b29 100644 --- a/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/ticketGroups/groups/groupActionMenu/groupSettingsForm/groupSettingsForm.component.tsx +++ b/frontend/src/v5/ui/routes/viewer/tickets/ticketsForm/ticketGroups/groups/groupActionMenu/groupSettingsForm/groupSettingsForm.component.tsx @@ -133,6 +133,7 @@ export const GroupSettingsForm = ({ value, onSubmit, onCancel, prefixes, isColor formState: { errors, isValid, isDirty }, setValue, getValues, + trigger, } = formData; const getFormIsValid = () => { @@ -221,6 +222,10 @@ export const GroupSettingsForm = ({ value, onSubmit, onCancel, prefixes, isColor }); }, []); + useEffect(() => { + if (!isDirty) return; + trigger(); + }, [isSmart, isDirty]); useEffect(() => { setCurrentPrefixes(mergePrefixes(prefixes, subPrefixes(newPrefix))); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 32a7a62a289..d1448f6bfff 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -12528,10 +12528,10 @@ react-highlight-words@0.17.0: memoize-one "^4.0.0" prop-types "^15.5.8" -react-hook-form@7.12.2: - version "7.12.2" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.12.2.tgz#2660afbf03c4ef360a9314ebf46ce3d972296c77" - integrity sha512-cpxocjrgpMAJCMJQR51BQhMoEx80/EQqePNihMTgoTYTqCRbd2GExi+N4GJIr+cFqrmbwNj9wxk5oLWYQsUefg== +react-hook-form@7.61.1: + version "7.61.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.61.1.tgz#8c1f086ccd921a6e90df6800787e9ca3833f86c1" + integrity sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew== react-intl@5.21.0: version "5.21.0"