diff --git a/.babelrc b/.babelrc deleted file mode 100644 index e062e6bc84..0000000000 --- a/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - "@babel/preset-env", - "@babel/react" - ], - "plugins": [ - [ - "formatjs", - { - "idInterpolationPattern": "[sha512:contenthash:base64:6]", - "ast": true - } - ] - ] -} diff --git a/Dockerfile b/Dockerfile index 699c3a0d7e..1987602164 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,7 @@ WORKDIR /app # copy configuration/build files COPY ./build /app/build/ -COPY ./*.json ./*.js ./.babelrc /app/ +COPY ./*.json ./*.js /app/ # install WITH dev tooling RUN npm ci --legacy-peer-deps diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000000..9d62b4d612 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + presets: ['@babel/preset-env', '@babel/react'], + plugins: [ + [ + 'formatjs', + { + idInterpolationPattern: '[sha512:contenthash:base64:6]', + ast: true, + }, + ], + ], +}; diff --git a/package-lock.json b/package-lock.json index 220f42504c..d688aa14f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", "@open-formulieren/design-tokens": "^0.39.0", - "@open-formulieren/formio-builder": "^0.7.2", + "@open-formulieren/formio-builder": "^0.8.0", "@rjsf/core": "^4.2.1", "@tinymce/tinymce-react": "^3.12.6", "@trivago/prettier-plugin-sort-imports": "^4.0.0", @@ -3956,9 +3956,9 @@ "integrity": "sha512-tj8OzDBGilvdbJJVrOCkfQhIWClCoijqxYup1W0Sm612ernPXctJMldjSRDgDjNRqXRlXVD9kP3+6PjZiWhy4w==" }, "node_modules/@open-formulieren/formio-builder": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.7.2.tgz", - "integrity": "sha512-6/LM18IA39DGPQPNsbam0NaK904vX6rGEu7PRRJKoXhKeTJZ1T4H/Z6rLSB1EFhhGb+RNYHVQB3YIDsyes8+4w==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.8.0.tgz", + "integrity": "sha512-MzzPhP9jG6PhUfdGzdLAGkutAC3PgOVObtzpaxAPqdKbgQ0p1niTKeLpgvI49J0ofcLJm9z5t/Z6o1eoGmkyzA==", "dependencies": { "@emotion/css": "^11.11.2", "clsx": "^1.2.1", @@ -3966,9 +3966,8 @@ "lodash.camelcase": "^4.3.0", "lodash.clonedeep": "^4.5.0", "lodash.debounce": "^4.0.8", - "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", - "lodash.set": "^4.3.2", + "lodash.merge": "^4.6.2", "lodash.uniqueid": "^4.0.1", "react-intl": "^6.3.2", "react-select": "^5.7.2", @@ -22223,11 +22222,6 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -22239,6 +22233,11 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", @@ -34480,9 +34479,9 @@ "integrity": "sha512-tj8OzDBGilvdbJJVrOCkfQhIWClCoijqxYup1W0Sm612ernPXctJMldjSRDgDjNRqXRlXVD9kP3+6PjZiWhy4w==" }, "@open-formulieren/formio-builder": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.7.2.tgz", - "integrity": "sha512-6/LM18IA39DGPQPNsbam0NaK904vX6rGEu7PRRJKoXhKeTJZ1T4H/Z6rLSB1EFhhGb+RNYHVQB3YIDsyes8+4w==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.8.0.tgz", + "integrity": "sha512-MzzPhP9jG6PhUfdGzdLAGkutAC3PgOVObtzpaxAPqdKbgQ0p1niTKeLpgvI49J0ofcLJm9z5t/Z6o1eoGmkyzA==", "requires": { "@emotion/css": "^11.11.2", "clsx": "^1.2.1", @@ -34490,9 +34489,8 @@ "lodash.camelcase": "^4.3.0", "lodash.clonedeep": "^4.5.0", "lodash.debounce": "^4.0.8", - "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", - "lodash.set": "^4.3.2", + "lodash.merge": "^4.6.2", "lodash.uniqueid": "^4.0.1", "react-intl": "^6.3.2", "react-select": "^5.7.2", @@ -48659,11 +48657,6 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" - }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -48675,6 +48668,11 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", diff --git a/package.json b/package.json index c9af8444aa..b686517aeb 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", "@open-formulieren/design-tokens": "^0.39.0", - "@open-formulieren/formio-builder": "^0.7.2", + "@open-formulieren/formio-builder": "^0.8.0", "@rjsf/core": "^4.2.1", "@tinymce/tinymce-react": "^3.12.6", "@trivago/prettier-plugin-sort-imports": "^4.0.0", @@ -66,6 +66,10 @@ ], "modulePaths": [ "/src/openforms/js" + ], + "transformIgnorePatterns": [ + "/node_modules/(?!@open-formulieren/formio-builder)/", + "\\.pnp\\.[^\\\/]+$" ] }, "browserslist": [ diff --git a/src/openforms/forms/tests/e2e_tests/test_form_designer.py b/src/openforms/forms/tests/e2e_tests/test_form_designer.py index fd4f8b8553..1fbc4b15f1 100644 --- a/src/openforms/forms/tests/e2e_tests/test_form_designer.py +++ b/src/openforms/forms/tests/e2e_tests/test_form_designer.py @@ -133,24 +133,19 @@ def setUpTestData(): locale="nl" ) # check the values of the translation inputs - literal1 = page.locator( - f'css=[name="{_translations_data_path}[0]{self.translations_literal_suffix}"]' - ) + literal1 = page.locator(f'css=[name="{_translations_data_path}[0]"]') translation1 = page.locator( - f'css=[name="{_translations_data_path}[0]{self.translations_translation_suffix}"]' - ) - literal2 = page.locator( - f'css=[name="{_translations_data_path}[1]{self.translations_literal_suffix}"]' + f'css=[name="{_translations_data_path}[0][translation]"]' ) + literal2 = page.locator(f'css=[name="{_translations_data_path}[1]"]') translation2 = page.locator( - f'css=[name="{_translations_data_path}[1]{self.translations_translation_suffix}"]' - ) - literal3 = page.locator( - f'css=[name="{_translations_data_path}[2]{self.translations_literal_suffix}"]' + f'css=[name="{_translations_data_path}[1][translation]"]' ) + literal3 = page.locator(f'css=[name="{_translations_data_path}[2]"]') translation3 = page.locator( - f'css=[name="{_translations_data_path}[2]{self.translations_translation_suffix}"]' + f'css=[name="{_translations_data_path}[2][translation]"]' ) + await expect(literal1).to_have_value("Field 1") await expect(translation1).to_have_value("") await expect(literal2).to_have_value("Description 1") @@ -168,21 +163,12 @@ def setUpTestData(): "link", name=self._translate("Vertalingen") ).click() - # React-based form builder keeps translations order consistent/fixed - if self.is_translations_order_fixed: - label_literal = literal1 - label_translation = translation1 - description_literal = literal2 - description_translation = translation2 - tooltip_literal = literal3 - tooltip_translation = translation3 - else: - label_literal = literal3 - label_translation = translation3 - description_literal = literal1 - description_translation = translation1 - tooltip_literal = literal2 - tooltip_translation = translation2 + label_literal = literal3 + label_translation = translation3 + description_literal = literal1 + description_translation = translation1 + tooltip_literal = literal2 + tooltip_translation = translation2 await expect(label_literal).to_have_value("Field label") await expect(label_translation).to_have_value("") @@ -582,6 +568,202 @@ def setUp(self): config.enable_react_formio_builder = True config.save() + @staticmethod + async def _check_translation( + page: Page, + prop: str, + label: str, + expected_literal: str, + expected_translation: str, + ): + # there's no built in get_by_description :( + label_id = await page.get_by_text(label, exact=True).get_attribute("id") + literal_ = page.locator(f'css=[aria-describedby="{label_id}"]') + await expect(literal_).to_have_text(expected_literal) + translation_field = page.get_by_label(f'Translation for "{prop}"', exact=True) + await expect(translation_field).to_have_value(expected_translation) + return translation_field + + async def test_editing_translatable_properties(self): + # completely overridden instead of sharing the test with the old builder - the + # test code became unmaintainable + + @sync_to_async + def setUpTestData(): + # set up a form + form = FormFactory.create( + name="Playwright test", + name_nl="Playwright test", + generate_minimal_setup=True, + formstep__form_definition__name_nl="Playwright test", + formstep__form_definition__configuration={ + "components": [ + { + "type": "textfield", + "key": "field1", + "label": "Field 1", + "description": "Description 1", + "tooltip": "Tooltip 1", + }, + { + "type": "select", + "key": "field2", + "label": "Field 2", + "description": "Description 2", + "tooltip": "Tooltip 2", + "data": { + "values": [ + {"value": "option1", "label": "Option 1"}, + {"value": "option2", "label": "Option 2"}, + ] + }, + }, + ], + }, + ) + return form + + await create_superuser() + form = await setUpTestData() + admin_url = str( + furl(self.live_server_url) + / reverse("admin:forms_form_change", args=(form.pk,)) + ) + + async with browser_page() as page: + await self._admin_login(page) + await page.goto(str(admin_url)) + await page.get_by_role("tab", name="Steps and fields").click() + + with phase("Textfield component checks"): + await open_component_options_modal(page, "Field 1") + + # find and click translations tab + await page.get_by_role( + "link", name=self._translate("Vertalingen") + ).click() + + # check the values of the translation inputs + await self._check_translation(page, "label", "Label", "Field 1", "") + await self._check_translation( + page, "description", "Description", "Description 1", "" + ) + await self._check_translation( + page, "tooltip", "Tooltip", "Tooltip 1", "" + ) + + # edit textfield label literal + await page.get_by_role("link", name=self._translate("Basis")).click() + await page.get_by_label("Label").fill("Field label") + + # translations tab needs to be updated - note that the react-base builder + # preserves the order of literals/translations + await page.get_by_role( + "link", name=self._translate("Vertalingen") + ).click() + + # React-based form builder has a more accessible translations table + label_translation = await self._check_translation( + page, "label", "Label", "Field label", "" + ) + await self._check_translation( + page, "description", "Description", "Description 1", "" + ) + await self._check_translation( + page, "tooltip", "Tooltip", "Tooltip 1", "" + ) + + # enter translations and save + await label_translation.fill("Veldlabel") + modal = page.locator("css=.formio-dialog-content") + await modal.get_by_role( + "button", name=self._translate("Opslaan"), exact=True + ).click() + + # TODO: this still uses the old translation mechanism, will follow in a later + # version of @open-formulieren/formio-builder npm package. + with phase("Select component checks"): + await open_component_options_modal(page, "Field 2") + # find and click translations tab + await page.get_by_role("link", name="Vertalingen").click() + + expected_literals = [ + "Field 2", + "Description 2", + "Tooltip 2", + "Option 1", + "Option 2", + ] + for index, literal in enumerate(expected_literals): + with self.subTest(literal=literal, index=index): + literal_loc = page.locator( + f'css=[name="data[openForms.translations.nl][{index}]"]' + ) + await expect(literal_loc).to_have_value(literal) + + await page.get_by_role("button", name="Annuleren").click() + await expect(page.locator("css=.formio-dialog-content")).to_be_hidden() + + with phase("save form changes to backend"): + await page.get_by_role("button", name="Save", exact=True).click() + changelist_url = str( + furl(self.live_server_url) / reverse("admin:forms_form_changelist") + ) + await expect(page).to_have_url(changelist_url) + + @sync_to_async + def assertState(): + fd = form.formstep_set.get().form_definition + textfield = fd.configuration["components"][0] + + self.assertEqual( + textfield["openForms"]["translations"]["nl"]["label"], + "Veldlabel", + ) + self.assertEqual(fd.component_translations, {}) + + await assertState() + + @tag("gh-2800") + async def test_editing_translatable_properties_remembers_translations(self): + """ + Assert that entering translations and then changing the source string keeps the translation. + """ + await create_superuser() + admin_url = str(furl(self.live_server_url) / reverse("admin:forms_form_add")) + + async with browser_page() as page: + await self._admin_login(page) + await page.goto(str(admin_url)) + await add_new_step(page) + await drag_and_drop_component(page, "Tekstveld") + await expect(page.locator("css=.formio-dialog-content")).to_be_visible() + label_locator = page.get_by_label("Label", exact=True) + await label_locator.clear() + await label_locator.fill("Test") + + # Set an initial translation + await page.get_by_role("link", name=self._translate("Vertalingen")).click() + + translation = await self._check_translation( + page, "label", "Label", expected_literal="Test", expected_translation="" + ) + await translation.click() + await translation.fill("Vertaald label") + + # Now change the source string & check the translations are still in place + await page.get_by_role("link", name=self._translate("Basis")).click() + await page.get_by_label("Label", exact=True).fill("Test 2") + + await page.get_by_role("link", name=self._translate("Vertalingen")).click() + await self._check_translation( + page, + "label", + "Label", + expected_literal="Test 2", + expected_translation="Vertaald label", + ) + class FormDesignerRegressionTests(E2ETestCase): async def test_user_defined_variable_boolean_initial_value_false(self): diff --git a/src/openforms/js/components/admin/form_design/form-creation-form.js b/src/openforms/js/components/admin/form_design/form-creation-form.js index 894ce84813..765d6e1b81 100644 --- a/src/openforms/js/components/admin/form_design/form-creation-form.js +++ b/src/openforms/js/components/admin/form_design/form-creation-form.js @@ -16,6 +16,7 @@ import Fieldset from 'components/admin/forms/Fieldset'; import ValidationErrorsProvider from 'components/admin/forms/ValidationErrors'; import { clearObsoleteLiterals, + isNewBuilderComponent, persistComponentTranslations, } from 'components/formio_builder/translation'; import {APIError, NotAuthenticatedError} from 'utils/exception'; @@ -24,7 +25,7 @@ import {getUniqueRandomString} from 'utils/random'; import Appointments, {KEYS as APPOINTMENT_CONFIG_KEYS} from './Appointments'; import Confirmation from './Confirmation'; -import {APIContext, FormContext} from './Context'; +import {APIContext, FeatureFlagsContext, FormContext} from './Context'; import DataRemoval from './DataRemoval'; import FormConfigurationFields from './FormConfigurationFields'; import FormDetailFields from './FormDetailFields'; @@ -424,7 +425,8 @@ function reducer(draft, action) { break; } case 'EDIT_STEP_COMPONENT_MUTATED': { - const {mutationType, schema, args, formDefinition} = action.payload; + const {mutationType, schema, args, formDefinition, reactFormioBuilderEnabled} = + action.payload; let originalComp; let isNew; @@ -475,8 +477,13 @@ function reducer(draft, action) { // the step component translations container, and then using the full form // configuration, we can remove the translations for literals that are no longer // present/used. - persistComponentTranslations(step.componentTranslations, schema); - step.componentTranslations = clearObsoleteLiterals(step.componentTranslations, configuration); + if (!reactFormioBuilderEnabled || !isNewBuilderComponent(schema)) { + persistComponentTranslations(step.componentTranslations, schema); + step.componentTranslations = clearObsoleteLiterals( + step.componentTranslations, + configuration + ); + } if (!isNew) { // In the case the component was removed, originalComp is null @@ -950,6 +957,7 @@ StepsFieldSet.propTypes = { */ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl}) => { const {csrftoken} = useContext(APIContext); + const {react_formio_builder_enabled = false} = useContext(FeatureFlagsContext); const initialState = { ...initialFormState, form: { @@ -1067,6 +1075,7 @@ const FormCreationForm = ({formUuid, formUrl, formHistoryUrl}) => { schema, formDefinition, args: rest, + reactFormioBuilderEnabled: react_formio_builder_enabled, }, }); }; diff --git a/src/openforms/js/components/formio_builder/index.js b/src/openforms/js/components/formio_builder/index.js index c0c1afaf83..82f6518966 100644 --- a/src/openforms/js/components/formio_builder/index.js +++ b/src/openforms/js/components/formio_builder/index.js @@ -7,6 +7,7 @@ import ReactDOM from 'react-dom'; import {FeatureFlagsContext} from 'components/admin/form_design/Context'; import { clearObsoleteLiterals, + isNewBuilderComponent, persistComponentTranslations, } from 'components/formio_builder/translation'; import {onLoaded} from 'utils/dom'; @@ -23,6 +24,7 @@ export const ELEMENT_CONTAINER = 'container'; onLoaded(() => { const FORM_BUILDERS = BEM.getBEMNodes(BLOCK_FORM_BUILDER); const featureFlags = FORM_BUILDERS.length ? jsonScriptToVar('feature-flags') : {}; + const {react_formio_builder_enabled = false} = featureFlags; [...FORM_BUILDERS].forEach(node => { const configurationInput = BEM.getChildBEMNode(node, BLOCK_FORM_BUILDER, 'configuration-input'); @@ -59,9 +61,11 @@ onLoaded(() => { throw new Error(`Unknown mutation type '${mutationType}'`); } - persistComponentTranslations(componentTranslations, schema); - componentTranslations = clearObsoleteLiterals(componentTranslations, configuration); - componentTranslationsInput.value = JSON.stringify(componentTranslations); + if (!react_formio_builder_enabled || !isNewBuilderComponent(schema)) { + persistComponentTranslations(componentTranslations, schema); + componentTranslations = clearObsoleteLiterals(componentTranslations, configuration); + componentTranslationsInput.value = JSON.stringify(componentTranslations); + } }; ReactDOM.render( diff --git a/src/openforms/js/components/formio_builder/translation.js b/src/openforms/js/components/formio_builder/translation.js index 75c472114e..ca1299b346 100644 --- a/src/openforms/js/components/formio_builder/translation.js +++ b/src/openforms/js/components/formio_builder/translation.js @@ -1,6 +1,7 @@ /** * Utilities to manage the translations/localisation of components in the form builder. */ +import BUILDER_REGISTRY from '@open-formulieren/formio-builder/esm/registry'; import Utils from 'formiojs/utils'; import get from 'lodash/get'; import isEmpty from 'lodash/isEmpty'; @@ -264,3 +265,7 @@ export const handleComponentValueLiterals = ( }); return translations; }; + +export const isNewBuilderComponent = component => { + return BUILDER_REGISTRY.hasOwnProperty(component.type); +};