import React, { useCallback, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { Button, Column, CopyButton, Dropdown, FileUploader, Grid, IconButton, InlineLoading, InlineNotification, Tab, TabList, TabPanel, TabPanels, Tabs, } from '@carbon/react'; import { ArrowLeft, Maximize, Minimize, Download } from '@carbon/react/icons'; import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { type TFunction } from 'i18next'; import { ConfigurableLink, showModal, useConfig } from '@openmrs/esm-framework'; import ActionButtons from '../action-buttons/action-buttons.component'; import AuditDetails from '../audit-details/audit-details.component'; import FormRenderer from '../form-renderer/form-renderer.component'; import Header from '../header/header.component'; import InteractiveBuilder from '../interactive-builder/interactive-builder.component'; import TranslationBuilder from '../translation-builder/translation-builder.component'; import SchemaEditor from '../schema-editor/schema-editor.component'; import ValidationMessage from '../validation-info/validation-info.component'; import { handleFormValidation } from '@resources/form-validator.resource'; import { mergeTranslatedSchema } from '../../utils/translationSchemaUtils'; import { useClobdata } from '@hooks/useClobdata'; import { useForm } from '@hooks/useForm'; import { useLanguageOptions } from '@hooks/getLanguageOptionsFromSession'; import type { IMarker } from 'react-ace'; import type { FormSchema } from '@openmrs/esm-form-engine-lib'; import type { Schema } from '@types'; import type { ConfigObject } from '../../config-schema'; import styles from './form-editor.scss'; interface ErrorProps { error: Error; title: string; } interface TranslationFnProps { t: TFunction; } interface MarkerProps extends IMarker { text: string; } type Status = 'idle' | 'formLoaded' | 'schemaLoaded'; const ErrorNotification = ({ error, title }: ErrorProps) => { return ( ); }; const FormEditorContent: React.FC = ({ t }) => { const defaultEnterDelayInMs = 300; const { formUuid } = useParams<{ formUuid: string }>(); const { blockRenderingWithErrors, dataTypeToRenderingMap } = useConfig(); const isNewSchema = !formUuid; const [schema, setSchema] = useState(); const { form, formError, isLoadingForm } = useForm(formUuid); const { clobdata, clobdataError, isLoadingClobdata } = useClobdata(form); const [status, setStatus] = useState('idle'); const [isMaximized, setIsMaximized] = useState(false); const [stringifiedSchema, setStringifiedSchema] = useState(schema ? JSON.stringify(schema, null, 2) : ''); const [validationResponse, setValidationResponse] = useState([]); const [isValidating, setIsValidating] = useState(false); const [validationComplete, setValidationComplete] = useState(false); const [publishedWithErrors, setPublishedWithErrors] = useState(false); const [errors, setErrors] = useState>([]); const [validationOn, setValidationOn] = useState(false); const [invalidJsonErrorMessage, setInvalidJsonErrorMessage] = useState(''); const languageOptions = useLanguageOptions(); const [selectedLanguageCode, setSelectedLanguageCode] = useState(() => languageOptions[0]?.code ?? 'en'); const [shouldMergeTranslation, setShouldMergeTranslation] = useState(false); const [renderLangCode, setRenderLangCode] = useState(null); const isLoadingFormOrSchema = Boolean(formUuid) && (isLoadingClobdata || isLoadingForm); const langCodeForPreview = useMemo( () => (shouldMergeTranslation ? renderLangCode : null), [shouldMergeTranslation, renderLangCode], ); const resetErrorMessage = useCallback(() => { setInvalidJsonErrorMessage(''); }, []); const handleSchemaChange = useCallback( (updatedSchema: string) => { resetErrorMessage(); setStringifiedSchema(updatedSchema); }, [resetErrorMessage], ); const updateSchema = useCallback((updatedSchema: FormSchema) => { setSchema(updatedSchema); localStorage.setItem('formJSON', JSON.stringify(updatedSchema)); }, []); const launchRestoreDraftSchemaModal = useCallback(() => { const dispose = showModal('restore-draft-schema-modal', { closeModal: () => dispose(), onSchemaChange: updateSchema, }); }, [updateSchema]); useEffect(() => { if (formUuid) { if (form && Object.keys(form).length > 0) { setStatus('formLoaded'); } if (status === 'formLoaded' && !isLoadingClobdata && clobdata === undefined) { launchRestoreDraftSchemaModal(); } if (clobdata && Object.keys(clobdata).length > 0) { setStatus('schemaLoaded'); setSchema(clobdata); localStorage.setItem('formJSON', JSON.stringify(clobdata)); } } }, [clobdata, form, formUuid, isLoadingClobdata, isLoadingFormOrSchema, launchRestoreDraftSchemaModal, status]); useEffect(() => { setStringifiedSchema(JSON.stringify(schema, null, 2)); }, [schema]); const onValidateForm = async () => { setIsValidating(true); try { const [errorsArray] = await handleFormValidation(schema, dataTypeToRenderingMap); setValidationResponse(errorsArray); setValidationComplete(true); } catch (error) { console.error('Error during form validation:', error); } finally { setIsValidating(false); } }; const inputDummySchema = useCallback(() => { const dummySchema: FormSchema = { encounterType: '', name: 'Sample Form', processor: 'EncounterFormProcessor', referencedForms: [], uuid: '', version: '1.0', pages: [ { label: 'First Page', sections: [ { label: 'A Section', isExpanded: 'true', questions: [ { id: 'sampleQuestion', label: 'A Question of type obs that renders a text input', type: 'obs', questionOptions: { rendering: 'text', concept: 'a-system-defined-concept-uuid', }, }, ], }, { label: 'Another Section', isExpanded: 'true', questions: [ { id: 'anotherSampleQuestion', label: 'Another Question of type obs whose answers get rendered as radio inputs', type: 'obs', questionOptions: { rendering: 'radio', concept: 'system-defined-concept-uuid', answers: [ { concept: 'another-system-defined-concept-uuid', label: 'Choice 1', }, { concept: 'yet-another-system-defined-concept-uuid', label: 'Choice 2', }, { concept: 'yet-one-more-system-defined-concept-uuid', label: 'Choice 3', }, ], }, }, ], }, ], }, ], }; setStringifiedSchema(JSON.stringify(dummySchema, null, 2)); updateSchema({ ...dummySchema }); }, [updateSchema]); const renderSchemaChanges = useCallback(() => { resetErrorMessage(); { try { const parsedJson: Schema = JSON.parse(stringifiedSchema); updateSchema(parsedJson); setStringifiedSchema(JSON.stringify(parsedJson, null, 2)); } catch (e) { if (e instanceof Error) { setInvalidJsonErrorMessage(e.message); } } } }, [stringifiedSchema, updateSchema, resetErrorMessage]); const translatedSchema = useMemo(() => { if (!schema) return null; if (!shouldMergeTranslation) return schema; return mergeTranslatedSchema(schema, langCodeForPreview); }, [schema, shouldMergeTranslation, langCodeForPreview]); const handleRenderSchemaChanges = useCallback(() => { if (errors.length && blockRenderingWithErrors) { setValidationOn(true); return; } setShouldMergeTranslation(true); setRenderLangCode(selectedLanguageCode); if (errors.length && !blockRenderingWithErrors) { setValidationOn(true); renderSchemaChanges(); } else { renderSchemaChanges(); } }, [blockRenderingWithErrors, errors.length, renderSchemaChanges, selectedLanguageCode]); const handleSchemaImport = (event: React.ChangeEvent) => { const file = event.target.files[0]; const reader = new FileReader(); reader.onload = (e) => { const result = e.target?.result; if (typeof result === 'string') { const fileContent: string = result; const parsedJson: Schema = JSON.parse(fileContent); setSchema(parsedJson); } else if (result instanceof ArrayBuffer) { const decoder = new TextDecoder(); const fileContent: string = decoder.decode(result); const parsedJson: Schema = JSON.parse(fileContent); setSchema(parsedJson); } }; reader.readAsText(file); }; const downloadableSchema = useMemo( () => new Blob([JSON.stringify(schema, null, 2)], { type: 'application/json', }), [schema], ); const handleCopySchema = useCallback(async () => { await navigator.clipboard.writeText(stringifiedSchema); }, [stringifiedSchema]); const handleToggleMaximize = () => { setIsMaximized(!isMaximized); }; const responsiveSize = isMaximized ? 16 : 8; return (
{isLoadingFormOrSchema ? ( ) : (

{form?.name}

)}
{t('schemaEditor', 'Schema editor')}
{!schema ? ( ) : null} {isNewSchema && !schema ? ( ) : null} item?.label ?? ''} label={t('selectLanguage', 'Select language')} onChange={({ selectedItem }) => { if (selectedItem) setSelectedLanguageCode(selectedItem.code); }} titleText={t('previewFormIn', 'Preview form in')} selectedItem={languageOptions.find((opt) => opt.code === selectedLanguageCode)} type="inline" className={styles.dropdown} />
{schema ? ( <> {isMaximized ? : } ) : null}
{formError ? ( ) : null} {clobdataError ? ( ) : null}
{validationComplete && ( 0} publishedWithErrors={publishedWithErrors} errorsCount={validationResponse.length} /> )} {t('preview', 'Preview')} {t('interactiveBuilder', 'Interactive Builder')} {t('translationBuilder', 'Translation Builder')} {form && {t('auditDetails', 'Audit Details')}} {form && }
); }; function BackButton({ t }: TranslationFnProps) { return (
); } function FormEditor() { const { t } = useTranslation(); return ( <>
); } export default FormEditor;