diff --git a/package.json b/package.json index a67024fad1..30dcf2a2e6 100644 --- a/package.json +++ b/package.json @@ -123,8 +123,7 @@ "resolutions": { "postcss": "8.4.40", "parse-url": "8.1.0", - "@types/react": "17", - "@editorjs/editorjs": "2.27.2" + "@types/react": "17" }, "msw": { "workerDirectory": "public" diff --git a/packages/prosess-vedtak/package.json b/packages/prosess-vedtak/package.json index 9f1290c807..d6dcd8a6b8 100644 --- a/packages/prosess-vedtak/package.json +++ b/packages/prosess-vedtak/package.json @@ -5,7 +5,7 @@ "license": "MIT", "private": true, "dependencies": { - "@editorjs/editorjs": "2.30.3", + "@editorjs/editorjs": "2.30.2", "@editorjs/header": "2.8.7", "@editorjs/list": "1.9.0", "@editorjs/paragraph": "2.11.6", diff --git a/packages/prosess-vedtak/src/components/FritekstBrevPanel.tsx b/packages/prosess-vedtak/src/components/FritekstBrevPanel.tsx index 2e44358049..351d4a8ceb 100644 --- a/packages/prosess-vedtak/src/components/FritekstBrevPanel.tsx +++ b/packages/prosess-vedtak/src/components/FritekstBrevPanel.tsx @@ -1,6 +1,6 @@ import { Alert, Heading } from '@navikt/ds-react'; import { FormikProps, FormikValues } from 'formik'; -import React from 'react'; +import React, { useCallback } from 'react'; import { FormattedMessage, IntlShape, injectIntl } from 'react-intl'; import { TextAreaFormik, TextFieldFormik } from '@fpsak-frontend/form'; @@ -59,10 +59,14 @@ const FritekstBrevPanel = ({ const [featureToggles] = useFeatureToggles(); const kanRedigereFritekstbrev = kanHaManueltFritekstbrev(tilgjengeligeVedtaksbrev); - const handleFritekstSubmit = async (html: string, request) => { - formikProps.setFieldValue(fieldnames.REDIGERT_HTML, html); - await lagreDokumentdata(request); - }; + // useCallback to avoid re-initializing FritekstRedigering editorjs on every re-render of this component + const handleFritekstSubmit = useCallback( + async (html: string, request) => { + await formikProps.setFieldValue(fieldnames.REDIGERT_HTML, html); + lagreDokumentdata(request); + }, + [formikProps.setFieldValue, lagreDokumentdata], + ); return (
diff --git a/packages/prosess-vedtak/src/components/FritekstRedigering/EditorJSWrapper.ts b/packages/prosess-vedtak/src/components/FritekstRedigering/EditorJSWrapper.ts index 4cc5db6829..5356f156b8 100644 --- a/packages/prosess-vedtak/src/components/FritekstRedigering/EditorJSWrapper.ts +++ b/packages/prosess-vedtak/src/components/FritekstRedigering/EditorJSWrapper.ts @@ -1,4 +1,4 @@ -import EditorJS, { API } from '@editorjs/editorjs'; +import EditorJS, { API, type EditorConfig } from '@editorjs/editorjs'; import Header from '@editorjs/header'; import Paragraph from '@editorjs/paragraph'; import List from '@editorjs/list'; @@ -7,8 +7,8 @@ import edjsHTML from 'editorjs-html'; export default class EditorJSWrapper { private editor: EditorJS; - public async init({ holder, onChange }: { holder: string; onChange: (api: API, event: CustomEvent) => void }) { - const tools = { + constructor({ holder, onChange }: { holder: string; onChange: (api: API, event: CustomEvent) => void }) { + const tools: EditorConfig['tools'] = { paragraph: { class: Paragraph, inlineToolbar: true, @@ -34,7 +34,6 @@ export default class EditorJSWrapper { }, }, }; - this.editor = new EditorJS({ holder, minHeight: 0, @@ -43,13 +42,6 @@ export default class EditorJSWrapper { }); } - public harEditor() { - if (this.editor) { - return true; - } - return false; - } - public async importer(html) { await this.editor.isReady; await this.editor.blocks.renderFromHTML(html); @@ -57,14 +49,12 @@ export default class EditorJSWrapper { } public async erKlar() { - if (!this.editor) return false; - return this.editor; + return this.editor.isReady; } public async lagre() { - return this.editor.save().then(innhold => { - const edjsParser = edjsHTML(); - return edjsParser.parse(innhold).join(''); - }); + const innhold = await this.editor.save(); + const edjsParser = edjsHTML(); + return edjsParser.parse(innhold).join(''); } } diff --git a/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.stories.tsx b/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.stories.tsx new file mode 100644 index 0000000000..479ca34531 --- /dev/null +++ b/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.stories.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import { fn, expect, userEvent, waitFor } from '@storybook/test'; +import { createIntl, IntlShape, RawIntlProvider } from 'react-intl'; +import FritekstEditor from './FritekstEditor.js'; +import messages from '../../../i18n/nb_NO.json'; + +const withRawIntlProvider = + (intl: IntlShape): Decorator => + Story => ( + + + + ); + +const intl = createIntl({ + locale: 'nb-NO', + messages, +}); + +const meta = { + title: 'prosess/prosess-vedtak/FritekstRedigering', + component: FritekstEditor, + decorators: [withRawIntlProvider(intl)], +} satisfies Meta; + +export default meta; + +export const Default: StoryObj = { + args: { + kanInkludereKalender: true, + skalBrukeOverstyrendeFritekstBrev: false, + readOnly: false, + redigerbartInnholdKlart: true, + redigerbartInnhold: 'Storybook default scenario', + originalHtml: 'OriginalHtml', + prefiksInnhold: '', + suffiksInnhold: '', + brevStiler: '', + handleSubmit: fn(), + lukkEditor: fn(), + handleForhåndsvis: fn(), + setFieldValue: fn(), + }, +}; + +const customizedContent = 'Storybook customized scenario'; +const addedContent = ' Added text'; +export const AvansertMedPlayTest: StoryObj = { + args: { + ...Default.args, + redigerbartInnhold: customizedContent, + originalHtml: 'Storybook customized scenario originalt innhold', + prefiksInnhold: 'Prefiks', + suffiksInnhold: 'Suffiks', + }, + play: async ({ canvas, args, canvasElement }) => { + const prefixEl = canvas.getByText('Prefiks'); + expect(prefixEl).toBeInTheDocument(); + const suffixEl = canvas.getByText('Suffiks'); + expect(suffixEl).toBeInTheDocument(); + const contentBlock = canvasElement.querySelector('#rediger-brev'); + await expect(contentBlock).toBeInTheDocument(); + + const submitBtn = canvas.getByRole('button', { name: 'Lagre og lukk' }); + expect(submitBtn).toBeInTheDocument(); + await userEvent.click(submitBtn, { delay: 100 }); + await waitFor(() => expect(args.handleSubmit).toHaveBeenCalledWith(`

${customizedContent}

`)); + const para = contentBlock.querySelector('.ce-paragraph.cdx-block'); + await userEvent.type(para, addedContent); + await userEvent.click(submitBtn); + await waitFor(() => expect(args.handleSubmit).toHaveBeenCalledWith(`

${customizedContent}${addedContent}

`)); + }, +}; diff --git a/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.tsx b/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.tsx index f53f44c74b..afb63cc759 100644 --- a/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.tsx +++ b/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstEditor.tsx @@ -2,7 +2,7 @@ import { VerticalSpacer } from '@fpsak-frontend/shared-components'; import { Cancel } from '@navikt/ds-icons'; import { Alert, Button, HGrid, Heading, Modal } from '@navikt/ds-react'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage, WrappedComponentProps, injectIntl } from 'react-intl'; import InkluderKalenderCheckbox from '../InkluderKalenderCheckbox'; import PreviewLink from '../PreviewLink'; @@ -16,7 +16,6 @@ interface ownProps { handleSubmit: (value: string) => void; lukkEditor: () => void; handleForhåndsvis: (event: React.SyntheticEvent, html: string) => void; - oppdaterFormFelt: (html: string) => void; setFieldValue: (field: string, value: any, shouldValidate?: boolean) => void; kanInkludereKalender: boolean; skalBrukeOverstyrendeFritekstBrev: boolean; @@ -29,13 +28,22 @@ interface ownProps { brevStiler: string; } -const editor = new EditorJSWrapper(); +const debounce = funksjon => { + let teller; + return function lagre(...args) { + const context = this; + if (teller) clearTimeout(teller); + teller = setTimeout(() => { + teller = null; + funksjon.apply(context, args); + }, 1000); + }; +}; const FritekstEditor = ({ handleSubmit, lukkEditor, handleForhåndsvis, - oppdaterFormFelt, setFieldValue, kanInkludereKalender, skalBrukeOverstyrendeFritekstBrev, @@ -50,66 +58,87 @@ const FritekstEditor = ({ }: ownProps & WrappedComponentProps) => { const [visAdvarsel, setVisAdvarsel] = useState(false); const [visValideringsFeil, setVisValideringsFeil] = useState(false); + const editorRef = useRef(null); + const lastSubmitHtml = useRef(redigerbartInnhold); + const initImportNotDone = useRef(true); + + // useCallback to avoid recreation of this on every re-render of component + const handleLagre = useCallback(async () => { + const editor = editorRef.current; + if (editor !== null) { + await editor.erKlar(); + const html = await editor.lagre(); + if (html !== lastSubmitHtml.current) { + handleSubmit(html); + lastSubmitHtml.current = html; + } + } + }, [handleSubmit]); - const handleLagre = async () => { - const html = await editor.lagre(); - handleSubmit(html); - }; - - const debounce = funksjon => { - let teller; - return function lagre(...args) { - const context = this; - if (teller) clearTimeout(teller); - teller = setTimeout(() => { - teller = null; - funksjon.apply(context, args); - }, 1000); - }; - }; - - const debouncedLagre = useCallback(debounce(handleLagre), []); + // useCallback to avoid recreation of this on every re-render of component + const debouncedLagre = useCallback(debounce(handleLagre), [handleLagre]); - const onChange = () => { + // useCallback to avoid recreation of this on every re-render of component, since that would require recreating the editor on every re-render. + const onChange = useCallback(() => { if (!readOnly) { debouncedLagre(); } - }; + }, [readOnly, debouncedLagre]); - const lastEditor = async () => { - await editor.init({ holder: 'rediger-brev', onChange }); - await editor.importer(redigerbartInnhold); - const html = await editor.lagre(); - oppdaterFormFelt(html); - }; + // Create new instance of editor (wrapper) when neccessary + useEffect(() => { + editorRef.current = new EditorJSWrapper({ holder: 'rediger-brev', onChange }); + }, [onChange]); + // Last innhold inn i editor ved første initialisering, eller viss redigerbartInnhold har blir endra utanfrå. useEffect(() => { - if (redigerbartInnholdKlart && !readOnly) { - lastEditor(); + const lastEditor = async (editor: EditorJSWrapper) => { + if (initImportNotDone.current || lastSubmitHtml.current !== redigerbartInnhold) { + await editor.importer(redigerbartInnhold); + initImportNotDone.current = false; + } + }; + const editor = editorRef.current; + if (editor !== null) { + if (redigerbartInnholdKlart && !readOnly) { + lastEditor(editor); + } + } else { + throw new Error(`Unexpectedly no editor instance available`); } - }, [redigerbartInnholdKlart, readOnly]); + }, [redigerbartInnhold, redigerbartInnholdKlart, readOnly]); - const handleLagreOgLukk = () => { - handleLagre(); + const handleLagreOgLukk = async () => { + await handleLagre(); lukkEditor(); }; const onForhåndsvis = async e => { - const html = await editor.lagre(); - const validert = await validerRedigertHtml.isValid(html); - - if (validert) { - setVisValideringsFeil(false); - handleForhåndsvis(e, html); + const editor = editorRef.current; + if (editor !== null) { + const html = await editor.lagre(); + const validert = await validerRedigertHtml.isValid(html); + + if (validert) { + setVisValideringsFeil(false); + handleForhåndsvis(e, html); + } else { + setVisValideringsFeil(true); + } } else { - setVisValideringsFeil(true); + throw new Error(`Fritekstredigering ikke initialisert. Kan ikke lage forhåndsvisning.`); } }; const handleTilbakestill = async () => { - await editor.importer(originalHtml); - setVisAdvarsel(false); - handleLagre(); + const editor = editorRef.current; + if (editor !== null) { + await editor.importer(originalHtml); + setVisAdvarsel(false); + await handleLagre(); + } else { + console.warn('Fritekstredigering ikke initialisert. Kan ikke tilbakestille.'); + } }; return ( diff --git a/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstRedigering.tsx b/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstRedigering.tsx index 3f0c614d7e..6d88ec1b16 100644 --- a/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstRedigering.tsx +++ b/packages/prosess-vedtak/src/components/FritekstRedigering/FritekstRedigering.tsx @@ -10,7 +10,7 @@ import { import { DokumentDataType } from '@k9-sak-web/types/src/dokumentdata'; import { Edit } from '@navikt/ds-icons'; import { Alert, Button, Heading, Modal } from '@navikt/ds-react'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage, WrappedComponentProps, injectIntl } from 'react-intl'; import { fieldnames } from '../../konstanter'; import FritekstEditor from './FritekstEditor'; @@ -119,19 +119,31 @@ const FritekstRedigering = ({ const lukkEditor = () => setVisRedigering(false); - const handleLagre = async html => { - handleSubmit( - html, - lagLagreHtmlDokumentdataRequest({ - dokumentdata, - redigerbarDokumentmal, - redigertHtml: html, - originalHtml, - inkluderKalender, - overstyrtMottaker, - }), - ); - }; + // useCallback for å unngå unødvendig re-initialisering av editorjs i FritekstEditor + const handleLagre = useCallback( + async html => { + handleSubmit( + html, + lagLagreHtmlDokumentdataRequest({ + dokumentdata, + redigerbarDokumentmal, + redigertHtml: html, + originalHtml, + inkluderKalender, + overstyrtMottaker, + }), + ); + }, + [ + handleSubmit, + lagLagreHtmlDokumentdataRequest, + dokumentdata, + redigerbarDokumentmal, + originalHtml, + inkluderKalender, + overstyrtMottaker, + ], + ); useEffect(() => { if (!firstRender.current && overstyrtMottaker && !henterMal) { @@ -154,8 +166,6 @@ const FritekstRedigering = ({ const handleForhåndsvis = (e: React.SyntheticEvent, html: string) => previewBrev(e, html); - const oppdaterFormFelt = (html: string) => setFieldValue(fieldnames.REDIGERT_HTML, html); - return ( <>

@@ -191,7 +201,6 @@ const FritekstRedigering = ({ handleSubmit={handleLagre} lukkEditor={lukkEditor} handleForhåndsvis={handleForhåndsvis} - oppdaterFormFelt={oppdaterFormFelt} setFieldValue={setFieldValue} kanInkludereKalender={kanInkludereKalender} skalBrukeOverstyrendeFritekstBrev={skalBrukeOverstyrendeFritekstBrev} diff --git a/yarn.lock b/yarn.lock index 30bc0402b7..004a124091 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1585,10 +1585,17 @@ __metadata: languageName: node linkType: hard -"@editorjs/editorjs@npm:2.27.2": - version: 2.27.2 - resolution: "@editorjs/editorjs@npm:2.27.2" - checksum: fd484610d8ba8f04aff5ac5331c7a0ec75796af5dc0251e13cf5e64fdbe72d53f91e35b146cb3aca23db7091bd70da8b1abf9bcfc834c07dcc79e1299e010d7a +"@editorjs/editorjs@npm:2.30.2": + version: 2.30.2 + resolution: "@editorjs/editorjs@npm:2.30.2" + checksum: 658e10918390d18578e65d73e84f601b7eab8ba1a704142872a597ed0da6bb1ee407fc32b5b18a473a99c993729dc8959df408f10738a6150199acaf225474fc + languageName: node + linkType: hard + +"@editorjs/editorjs@npm:^2.29.1": + version: 2.30.3 + resolution: "@editorjs/editorjs@npm:2.30.3" + checksum: 4cad89ba56abda96a00c74a3efe5d02d6cbbd44fda82793ad7b115ed0580d721100e98c972c9dcb0f15508064d8932f11a3b8dd6aa1ed60a28dc41b90ae847be languageName: node linkType: hard @@ -2684,7 +2691,7 @@ __metadata: version: 0.0.0-use.local resolution: "@fpsak-frontend/prosess-vedtak@workspace:packages/prosess-vedtak" dependencies: - "@editorjs/editorjs": 2.30.3 + "@editorjs/editorjs": 2.30.2 "@editorjs/header": 2.8.7 "@editorjs/list": 1.9.0 "@editorjs/paragraph": 2.11.6