diff --git a/jest.config.ts b/jest.config.ts index a26ebb6..0c69a57 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -11,6 +11,7 @@ export default { `!src/${AND_BELOW}/${DECLARATION_FILES}` ], // coverageProvider: 'v8', + silent: true, // All below console.error testEnvironment: 'jsdom', testMatch: [ `/test/${AND_BELOW}/${TEST_FILES}`, diff --git a/package-lock.json b/package-lock.json index d5d0f85..b9e0662 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,40 +8,47 @@ "name": "@enonic/react-components", "version": "0.0.0", "license": "Apache-2.0", - "dependencies": { - "domelementtype": "^2.3.0", - "html-react-parser": "^5.1.10", - "uri-js": "^4.4.1" - }, "devDependencies": { - "@enonic-types/core": "^7.14.4", + "@enonic-types/core": "^7.15.0-A1", "@enonic-types/guillotine": "^7.1.1", - "@enonic-types/lib-content": "^7.14.1", - "@enonic-types/lib-node": "^7.14.4", - "@enonic-types/lib-portal": "^7.14.4", + "@enonic-types/lib-content": "^7.15.0-A1", + "@enonic-types/lib-node": "^7.15.0-A1", + "@enonic-types/lib-portal": "^7.15.0-A1", + "@enonic/js-utils": "^1.8.1", "@jest/globals": "^29.7.0", - "@swc/core": "^1.7.26", + "@swc/core": "^1.10.0", "@testing-library/react": "^16.0.1", "@types/diffable-html": "^5.0.2", - "@types/react": "^18.3.10", - "concurrently": "^9.0.1", + "@types/react": "^18.3.13", + "clsx": "^2.1.1", + "concurrently": "^9.1.0", "cpy-cli": "^5.0.0", "del-cli": "^6.0.0", "diffable-html": "^5.0.0", + "domelementtype": "^2.3.0", "domhandler": "^5.0.3", + "esbuild-plugin-globals": "^0.2.0", "glob": "^11.0.0", + "html-react-parser": "^5.1.19", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "q-i": "^2.0.1", "react-test-renderer": "^18.2.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "tslib": "^2.7.0", + "tslib": "^2.8.1", "tsup": "^8.3.0", - "typescript": "^5.6.2" + "typescript": "^5.7.2", + "uri-js": "^4.4.1" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "*", + "@swc/core-linux-x64-gnu": "*" }, "peerDependencies": { - "@enonic-types/core": "^7", + "@enonic-types/core": "^7.15.0-A1", + "@enonic-types/lib-portal": "^7.15.0-A1", + "@enonic-types/lib-schema": "^7.15.0-A1", "prop-types": "*", "react": "*" } @@ -659,10 +666,10 @@ } }, "node_modules/@enonic-types/core": { - "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@enonic-types/core/-/core-7.14.4.tgz", - "integrity": "sha512-AzMe/DixiaNH8X3OEvy0AREwr3GcmmcQ5lQ3k8AQWK4rQVKmSVatuNslkQFvzakjc7BOcTHGSBxr5lhN9+qD8g==", - "dev": true + "version": "7.15.0-A1", + "resolved": "https://registry.npmjs.org/@enonic-types/core/-/core-7.15.0-A1.tgz", + "integrity": "sha512-Jay4mEh5mucuM8qu3E31/VBOmuzeeVSVzSnllwOVFAcuyvGqyIeMfVQP90WU7cmo6BmnLNelbD3vQamxGntvvA==", + "license": "Apache-2.0" }, "node_modules/@enonic-types/global": { "version": "7.14.3", @@ -681,96 +688,69 @@ "@enonic-types/lib-content": "^7.14.0" } }, - "node_modules/@enonic-types/lib-content": { + "node_modules/@enonic-types/guillotine/node_modules/@enonic-types/core": { "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@enonic-types/lib-content/-/lib-content-7.14.4.tgz", - "integrity": "sha512-LpvmQEme5Z1JbKQ36vVFGbUSgqBmILq3C9DP8OgXBwV7tekWkGOCKb+I1Kjw6j3wttMOZ/Upq8C2gsrNiTsA/A==", + "resolved": "https://registry.npmjs.org/@enonic-types/core/-/core-7.14.4.tgz", + "integrity": "sha512-AzMe/DixiaNH8X3OEvy0AREwr3GcmmcQ5lQ3k8AQWK4rQVKmSVatuNslkQFvzakjc7BOcTHGSBxr5lhN9+qD8g==", "dev": true, - "dependencies": { - "@enonic-types/core": "7.14.4" - } + "license": "Apache-2.0" }, - "node_modules/@enonic-types/lib-node": { + "node_modules/@enonic-types/guillotine/node_modules/@enonic-types/lib-content": { "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@enonic-types/lib-node/-/lib-node-7.14.4.tgz", - "integrity": "sha512-sEOzJu0zZN7SKY3sez8DqgVTLRsSzYI2G/DSLSg1GSzh+SI2uDSwtLwiu+jK1gj5LbQRTrUkIp3Edixtjee/Mw==", + "resolved": "https://registry.npmjs.org/@enonic-types/lib-content/-/lib-content-7.14.4.tgz", + "integrity": "sha512-LpvmQEme5Z1JbKQ36vVFGbUSgqBmILq3C9DP8OgXBwV7tekWkGOCKb+I1Kjw6j3wttMOZ/Upq8C2gsrNiTsA/A==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@enonic-types/core": "7.14.4" } }, - "node_modules/@enonic-types/lib-portal": { - "version": "7.14.4", - "resolved": "https://registry.npmjs.org/@enonic-types/lib-portal/-/lib-portal-7.14.4.tgz", - "integrity": "sha512-CO19Vzai4DGplgeOcqF6OAP0XvxufIDvkINFcy4AxZQgH5iigHVkdttWhaZRTW2meNoNR+dS367siRUozxspbA==", + "node_modules/@enonic-types/lib-content": { + "version": "7.15.0-A1", + "resolved": "https://registry.npmjs.org/@enonic-types/lib-content/-/lib-content-7.15.0-A1.tgz", + "integrity": "sha512-k810saMTSnUjMEUh7eZ1nhGIq6HkzwDULo5S71B4fpaw5TFG/gHM3Nx5S+HF7Cr/+AtCNZvKw/q0U7mfnkzOrQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@enonic-types/core": "7.14.4" + "@enonic-types/core": "7.15.0-A1" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", - "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", - "cpu": [ - "ppc64" - ], + "node_modules/@enonic-types/lib-node": { + "version": "7.15.0-A1", + "resolved": "https://registry.npmjs.org/@enonic-types/lib-node/-/lib-node-7.15.0-A1.tgz", + "integrity": "sha512-mw1NjGr7Y01xUsOIJMJ78A9pWI/lTFEOp7r/oSDs2Gkr3LsqaCQF2xVfbSkNxwfFRYI5+0ozrItRZXkQSLQ2kQ==", "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@enonic-types/core": "7.15.0-A1" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", - "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", - "cpu": [ - "arm" - ], + "node_modules/@enonic-types/lib-portal": { + "version": "7.15.0-A1", + "resolved": "https://registry.npmjs.org/@enonic-types/lib-portal/-/lib-portal-7.15.0-A1.tgz", + "integrity": "sha512-mKb3kBZJbCnzbzVkZxx57jUn322S6oE0JqwykEVRzzhoSpnvnE8f6ICVPjPjSgwSjX95ddtag4TcP2jcDQBo1g==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@enonic-types/core": "7.15.0-A1" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", - "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "node_modules/@enonic-types/lib-schema": { + "version": "7.15.0-A1", + "resolved": "https://registry.npmjs.org/@enonic-types/lib-schema/-/lib-schema-7.15.0-A1.tgz", + "integrity": "sha512-Ff+CmpHRrPnG2M55TQ1rApSLEzlQMUF52z8LTKGpj+N0XvkJvsRl8m79g38d5bWqxYRQxqNwwAI7Exaij5mqAw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@enonic-types/core": "7.15.0-A1" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", - "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", - "cpu": [ - "x64" - ], + "node_modules/@enonic/js-utils": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@enonic/js-utils/-/js-utils-1.8.1.tgz", + "integrity": "sha512-kVZeph1YSAxhmc077+GRrBOrU2ikDahByOk6FugTi2A25hBjUoEjZNSkjA3jHG1WG5Z97qI5tqcGaAyV/hdCYA==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } + "license": "Apache-2.0" }, "node_modules/@esbuild/darwin-arm64": { "version": "0.24.0", @@ -788,310 +768,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", - "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", - "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", - "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", - "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", - "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", - "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", - "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", - "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", - "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", - "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", - "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", - "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", - "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", - "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", - "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", - "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", - "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", - "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", - "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1612,6 +1288,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1625,6 +1302,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1651,6 +1329,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1664,6 +1343,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1677,6 +1357,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1690,6 +1371,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1703,6 +1385,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1716,6 +1399,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1729,6 +1413,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1742,6 +1427,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1755,6 +1441,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1768,19 +1455,20 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz", - "integrity": "sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], - "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1794,6 +1482,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1807,6 +1496,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1820,6 +1510,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1833,6 +1524,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1875,11 +1567,12 @@ } }, "node_modules/@swc/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.1.tgz", - "integrity": "sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.0.tgz", + "integrity": "sha512-+CuuTCmQFfzaNGg1JmcZvdUVITQXJk9sMnl1C2TiDLzOSVOJRwVD4dNo5dljX/qxpMAN+2BIYlwjlSkoGi6grg==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.17" @@ -1892,16 +1585,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.1", - "@swc/core-darwin-x64": "1.10.1", - "@swc/core-linux-arm-gnueabihf": "1.10.1", - "@swc/core-linux-arm64-gnu": "1.10.1", - "@swc/core-linux-arm64-musl": "1.10.1", - "@swc/core-linux-x64-gnu": "1.10.1", - "@swc/core-linux-x64-musl": "1.10.1", - "@swc/core-win32-arm64-msvc": "1.10.1", - "@swc/core-win32-ia32-msvc": "1.10.1", - "@swc/core-win32-x64-msvc": "1.10.1" + "@swc/core-darwin-arm64": "1.10.0", + "@swc/core-darwin-x64": "1.10.0", + "@swc/core-linux-arm-gnueabihf": "1.10.0", + "@swc/core-linux-arm64-gnu": "1.10.0", + "@swc/core-linux-arm64-musl": "1.10.0", + "@swc/core-linux-x64-gnu": "1.10.0", + "@swc/core-linux-x64-musl": "1.10.0", + "@swc/core-win32-arm64-msvc": "1.10.0", + "@swc/core-win32-ia32-msvc": "1.10.0", + "@swc/core-win32-x64-msvc": "1.10.0" }, "peerDependencies": { "@swc/helpers": "*" @@ -1913,13 +1606,14 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.1.tgz", - "integrity": "sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.0.tgz", + "integrity": "sha512-wCeUpanqZyzvgqWRtXIyhcFK3CqukAlYyP+fJpY2gWc/+ekdrenNIfZMwY7tyTFDkXDYEKzvn3BN/zDYNJFowQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -1929,13 +1623,14 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.1.tgz", - "integrity": "sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.0.tgz", + "integrity": "sha512-0CZPzqTynUBO+SHEl/qKsFSahp2Jv/P2ZRjFG0gwZY5qIcr1+B/v+o74/GyNMBGz9rft+F2WpU31gz2sJwyF4A==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -1945,13 +1640,14 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.1.tgz", - "integrity": "sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.0.tgz", + "integrity": "sha512-oq+DdMu5uJOFPtRkeiITc4kxmd+QSmK+v+OBzlhdGkSgoH3yRWZP+H2ao0cBXo93ZgCr2LfjiER0CqSKhjGuNA==", "cpu": [ "arm" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -1961,13 +1657,14 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.1.tgz", - "integrity": "sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.0.tgz", + "integrity": "sha512-Y6+PC8knchEViRxiCUj3j8wsGXaIhuvU+WqrFqV834eiItEMEI9+Vh3FovqJMBE3L7d4E4ZQtgImHCXjrHfxbw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1977,13 +1674,14 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.1.tgz", - "integrity": "sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.0.tgz", + "integrity": "sha512-EbrX9A5U4cECCQQfky7945AW9GYnTXtCUXElWTkTYmmyQK87yCyFfY8hmZ9qMFIwxPOH6I3I2JwMhzdi8Qoz7g==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -1999,7 +1697,7 @@ "cpu": [ "x64" ], - "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2009,13 +1707,14 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.1.tgz", - "integrity": "sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.0.tgz", + "integrity": "sha512-IEGvDd6aEEKEyZFZ8oCKuik05G5BS7qwG5hO5PEMzdGeh8JyFZXxsfFXbfeAqjue4UaUUrhnoX+Ze3M2jBVMHw==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2025,13 +1724,14 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.1.tgz", - "integrity": "sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.0.tgz", + "integrity": "sha512-UkQ952GSpY+Z6XONj9GSW8xGSkF53jrCsuLj0nrcuw7Dvr1a816U/9WYZmmcYS8tnG2vHylhpm6csQkyS8lpCw==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2041,13 +1741,14 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.1.tgz", - "integrity": "sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.0.tgz", + "integrity": "sha512-a2QpIZmTiT885u/mUInpeN2W9ClCnqrV2LnMqJR1/Fgx1Afw/hAtiDZPtQ0SqS8yDJ2VR5gfNZo3gpxWMrqdVA==", "cpu": [ "ia32" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2057,13 +1758,14 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.1.tgz", - "integrity": "sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.0.tgz", + "integrity": "sha512-tZcCmMwf483nwsEBfUk5w9e046kMa1iSik4bP9Kwi2FGtOfHuDfIcwW4jek3hdcgF5SaBW1ktnK/lgQLDi5AtA==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2072,17 +1774,36 @@ "node": ">=10" } }, + "node_modules/@swc/core/node_modules/@swc/core-linux-x64-gnu": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.0.tgz", + "integrity": "sha512-TaxpO6snTjjfLXFYh5EjZ78se69j2gDcqEM8yB9gguPYwkCHi2Ylfmh7iVaNADnDJFtjoAQp0L41bTV/Pfq9Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@swc/types": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" } @@ -2108,9 +1829,9 @@ } }, "node_modules/@testing-library/react": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.1.0.tgz", - "integrity": "sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", + "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.5" @@ -2120,10 +1841,10 @@ }, "peerDependencies": { "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2284,13 +2005,14 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "devOptional": true + "dev": true }, "node_modules/@types/react": { - "version": "18.3.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", - "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "devOptional": true, + "version": "18.3.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.13.tgz", + "integrity": "sha512-ii/gswMmOievxAJed4PAHT949bpYjPKXvXo1v6cRB/kqc2ZR4n+SgyCyvyc5Fec5ez8VnUumI1Vk7j6fRyRogg==", + "dev": true, + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2920,6 +2642,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -2986,6 +2718,7 @@ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.2", "lodash": "^4.17.21", @@ -3158,9 +2891,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -3199,7 +2932,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true + "dev": true }, "node_modules/data-urls": { "version": "3.0.2", @@ -3434,6 +3167,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, "funding": [ { "type": "github", @@ -3458,6 +3192,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -3530,10 +3265,17 @@ "dev": true }, "node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/error-ex": { "version": "1.3.2", @@ -3583,6 +3325,16 @@ "@esbuild/win32-x64": "0.24.0" } }, + "node_modules/esbuild-plugin-globals": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/esbuild-plugin-globals/-/esbuild-plugin-globals-0.2.0.tgz", + "integrity": "sha512-y+6utQVWrETQWs0J8EGLV5gEOP59mmjX+fKWoQHn4TYwFMaj0FxQYflc566tHuokBCzl+uNW2iIlM1o1jfNy6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=7" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -4007,6 +3759,8 @@ "version": "5.0.11", "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.11.tgz", "integrity": "sha512-iORudm2K0c0DYeEj4AbrG9PFzgp1dpFGkJUAiBlVTkeyaNf2YYIs1b0dF7rQUPnDZimkLx+Jls+CvRIKO/++Tg==", + "dev": true, + "license": "MIT", "dependencies": { "domhandler": "5.0.3", "htmlparser2": "9.1.0" @@ -4016,6 +3770,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -4029,6 +3785,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -4038,21 +3796,11 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/html-dom-parser/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/html-dom-parser/node_modules/htmlparser2": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -4060,6 +3808,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -4086,9 +3835,11 @@ "dev": true }, "node_modules/html-react-parser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.0.tgz", - "integrity": "sha512-t5IkGKTBYnjmYUzEV1RBT5twACepcf20u6Q0TylXQkb6NQKaBFSMebb/S/joxXrg8IHc+ROLirQIZc5DkD23hA==", + "version": "5.1.19", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.1.19.tgz", + "integrity": "sha512-ecjQg5KDhM+Yv3tRRfdp0fYSdSYHI1FQEDqou0g8NO7mXuoK8ksbYGRjeslqWO6QWX3PKREVWnC8VS1FSZaFHA==", + "dev": true, + "license": "MIT", "dependencies": { "domhandler": "5.0.3", "html-dom-parser": "5.0.11", @@ -4096,8 +3847,8 @@ "style-to-js": "1.1.16" }, "peerDependencies": { - "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19", - "react": "0.14 || 15 || 16 || 17 || 18 || 19" + "@types/react": "0.14 || 15 || 16 || 17 || 18", + "react": "0.14 || 15 || 16 || 17 || 18" }, "peerDependenciesMeta": { "@types/react": { @@ -4134,6 +3885,13 @@ "domelementtype": "1" } }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -4252,7 +4010,8 @@ "node_modules/inline-style-parser": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "dev": true }, "node_modules/is-arrayish": { "version": "0.2.1", @@ -6133,18 +5892,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6361,6 +6108,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } @@ -6483,7 +6231,8 @@ "node_modules/react-property": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", - "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==" + "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==", + "dev": true }, "node_modules/react-shallow-renderer": { "version": "16.15.0", @@ -6660,6 +6409,20 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.2.tgz", + "integrity": "sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7014,6 +6777,7 @@ "version": "1.1.16", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "dev": true, "dependencies": { "style-to-object": "1.0.8" } @@ -7022,6 +6786,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "dev": true, "dependencies": { "inline-style-parser": "0.2.4" } @@ -7449,7 +7214,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/tsup": { "version": "8.3.5", @@ -7566,6 +7332,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7635,6 +7402,8 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index d94c7f2..47c7722 100644 --- a/package.json +++ b/package.json @@ -9,31 +9,37 @@ ], "description": "Library of React components for handling Enonic XP data and page components", "devDependencies": { - "@enonic-types/core": "^7.14.4", + "@enonic-types/core": "^7.15.0-A1", "@enonic-types/guillotine": "^7.1.1", - "@enonic-types/lib-content": "^7.14.1", - "@enonic-types/lib-node": "^7.14.4", - "@enonic-types/lib-portal": "^7.14.4", + "@enonic-types/lib-content": "^7.15.0-A1", + "@enonic-types/lib-node": "^7.15.0-A1", + "@enonic-types/lib-portal": "^7.15.0-A1", + "@enonic/js-utils": "^1.8.1", "@jest/globals": "^29.7.0", - "@swc/core": "^1.7.26", + "@swc/core": "^1.10.0", "@testing-library/react": "^16.0.1", "@types/diffable-html": "^5.0.2", - "@types/react": "^18.3.10", - "concurrently": "^9.0.1", + "@types/react": "^18.3.13", + "clsx": "^2.1.1", + "concurrently": "^9.1.0", "cpy-cli": "^5.0.0", "del-cli": "^6.0.0", "diffable-html": "^5.0.0", + "domelementtype": "^2.3.0", "domhandler": "^5.0.3", + "esbuild-plugin-globals": "^0.2.0", "glob": "^11.0.0", + "html-react-parser": "^5.1.19", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "q-i": "^2.0.1", "react-test-renderer": "^18.2.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "tslib": "^2.7.0", + "tslib": "^2.8.1", "tsup": "^8.3.0", - "typescript": "^5.6.2" + "typescript": "^5.7.2", + "uri-js": "^4.4.1" }, "exports": { ".": { @@ -67,8 +73,14 @@ "main": "dist/index.cjs", "module": "dist/index.mjs", "name": "@enonic/react-components", + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "*", + "@swc/core-linux-x64-gnu": "*" + }, "peerDependencies": { - "@enonic-types/core": "^7", + "@enonic-types/core": "^7.15.0-A1", + "@enonic-types/lib-portal": "^7.15.0-A1", + "@enonic-types/lib-schema": "^7.15.0-A1", "prop-types": "*", "react": "*" }, @@ -96,10 +108,5 @@ }, "type": "module", "types": "dist/index.d.ts", - "dependencies": { - "domelementtype": "^2.3.0", - "html-react-parser": "^5.1.10", - "uri-js": "^4.4.1" - }, "version": "0.0.0" } diff --git a/src/Common/ErrorComponent.tsx b/src/Common/ErrorComponent.tsx new file mode 100644 index 0000000..ba4baf1 --- /dev/null +++ b/src/Common/ErrorComponent.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react'; + +import { CONTENT_STUDIO_EDIT_MODE_PLACEHOLDER_STYLE } from '../constants'; + +const PINK = '#e0b4b4'; +const DARK_RED = '#9f3a38'; + +const STYLE = { + ...CONTENT_STUDIO_EDIT_MODE_PLACEHOLDER_STYLE, + borderColor: PINK, + color: DARK_RED, +}; + +// NOTE: "Error" is a property on the global object in Ecmascript. +export function ErrorComponent({ + children, + html, + ...extraProps +}: { + children?: ReactNode + html?: string +}) { + if (html) { + return ( + // Using dangerouslySetInnerHTML avoids encoding < to < +
+ ); + } + return
{children}
+} diff --git a/src/Common/HtmlComment.tsx b/src/Common/HtmlComment.tsx new file mode 100644 index 0000000..56f81ba --- /dev/null +++ b/src/Common/HtmlComment.tsx @@ -0,0 +1,6 @@ +export function HtmlComment({ comment }: { comment: string }): JSX.Element { + return ( + // Using dangerouslySetInnerHTML avoids encoding < to < +
` }}/> + ); +} diff --git a/src/Common/Message.tsx b/src/Common/Message.tsx new file mode 100644 index 0000000..32a75c5 --- /dev/null +++ b/src/Common/Message.tsx @@ -0,0 +1,45 @@ +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; +import type {ReactNode} from 'react'; + +import { ErrorComponent } from './ErrorComponent'; +import { Warning } from './Warning'; +import { XP_REQUEST_MODE } from '../constants'; + + +export const Message = ({ + children, + mode, + ...extraProps +}: { + children: ReactNode, + mode?: LiteralUnion +}): JSX.Element | null => { + + // Log, but don't render errors in live and preview mode. + if (mode === XP_REQUEST_MODE.LIVE || mode === XP_REQUEST_MODE.PREVIEW) { + console.error(children); + return null; + } + + if (mode === XP_REQUEST_MODE.INLINE) { + return ( + + ); + } + + if (mode === XP_REQUEST_MODE.EDIT || mode === XP_REQUEST_MODE.ADMIN) { + return ( + + ); + } + + if (mode) { + console.error(`Unsupported mode passed to Message component! mode:${mode} children:${children}`); + } else { + console.warn(`Message component didn't get mode prop! children:${children}`); + } + return null; +} diff --git a/src/Common/TryCatch.tsx b/src/Common/TryCatch.tsx new file mode 100644 index 0000000..d20d60c --- /dev/null +++ b/src/Common/TryCatch.tsx @@ -0,0 +1,33 @@ +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; +import type {ReactNode} from 'react'; + +import * as React from 'react'; +import { Message } from './Message'; +import { XP_REQUEST_MODE } from '../constants'; + +export const TryCatch = ({ + children, + mode +}: { + children: ReactNode; + mode?: LiteralUnion +}): JSX.Element | null => { + try { + return <>{children}; + } catch (e) { + return ( + +

Error rendering component

+

{e.message}

+ { + mode === XP_REQUEST_MODE.EDIT && ( +
{e.stack || ''}
+ ) + } +
+ ); + } +} diff --git a/src/Common/Warning.tsx b/src/Common/Warning.tsx new file mode 100644 index 0000000..fca56a0 --- /dev/null +++ b/src/Common/Warning.tsx @@ -0,0 +1,22 @@ +import type {ReactNode} from 'react'; + +import { WARNING_STYLE } from '../constants'; + +export function Warning({ + children, + html, + ...extraProps +}: { + children?: ReactNode + html?: string +}) { + if (html) { + return ( + // Using dangerouslySetInnerHTML avoids encoding < to < +
+ ); + } + return ( +
{children}
+ ); +} diff --git a/src/ComponentRegistry/BaseComponent.tsx b/src/ComponentRegistry/BaseComponent.tsx new file mode 100644 index 0000000..34ed6cd --- /dev/null +++ b/src/ComponentRegistry/BaseComponent.tsx @@ -0,0 +1,117 @@ +import type {Component} from '@enonic-types/core'; +import type {ComponentRegistry} from './ComponentRegistry'; +import type {RenderableComponent} from '../types'; + +import { toStr } from '@enonic/js-utils/value/toStr'; +import * as React from 'react'; + +import { + RENDERABLE_COMPONENT_TYPE, + XP_REQUEST_MODE, +} from '../constants'; +import {ErrorComponent} from '../Common/ErrorComponent'; +import {Warning} from '../Common/Warning'; +import {BaseLayout} from './BaseLayout'; +import {BasePage} from './BasePage'; +import {BasePart} from './BasePart'; +import {BaseText} from './BaseText'; +import {BaseContentType} from './BaseContentType'; +import {XpFallback} from './XpFallback'; + + +export function BaseComponent({ + component, + componentRegistry +}: { + component: RenderableComponent + componentRegistry?: ComponentRegistry +}) { + // console.debug('BaseComponent component:', toStr(component)); + + if (!componentRegistry) { + console.warn('BaseComponent componentRegistry missing! with component:', toStr(component)); + return ( + + ); + } + + const { + type + } = component; + if (!type) { + console.error('BaseComponent component missing type:', toStr(component)); + return ( + + ); + } + // console.info('BaseComponent type:', type); + + switch (type) { + case RENDERABLE_COMPONENT_TYPE.PART: + return ( + + ); + case RENDERABLE_COMPONENT_TYPE.LAYOUT: + return ( + + ); + case RENDERABLE_COMPONENT_TYPE.PAGE: + return ( + + ); + case RENDERABLE_COMPONENT_TYPE.CONTENT_TYPE: + return ( + + ); + case RENDERABLE_COMPONENT_TYPE.TEXT: { + return ( + + ); + } + case RENDERABLE_COMPONENT_TYPE.ERROR: { + const { + html, + mode + } = component; + if (mode === XP_REQUEST_MODE.LIVE) { + return null; + } + return ( + + ); + } + case RENDERABLE_COMPONENT_TYPE.WARNING: { + const { + html, + mode + } = component; + if (mode === XP_REQUEST_MODE.LIVE) { + return null; + } + return ( + + ); + } + } // switch + + console.error(`Unknown component type: ${type}`); + return ( + + ); + +} // BaseComponent diff --git a/src/ComponentRegistry/BaseContentType.tsx b/src/ComponentRegistry/BaseContentType.tsx new file mode 100644 index 0000000..fb96939 --- /dev/null +++ b/src/ComponentRegistry/BaseContentType.tsx @@ -0,0 +1,60 @@ +import type { + ComponentRegistry, + RenderableContentType, +} from '../types'; + +import * as React from 'react'; +import { Message } from '../Common/Message'; +// import { XP_REQUEST_MODE } from '../constants'; + +export const BaseContentType = ({ + component, + componentRegistry +}: { + component: RenderableContentType + componentRegistry: ComponentRegistry +}): JSX.Element => { + const { + contentType, + mode, + props, + // NOTE: Such a warning would typically come from lib-react4xp DataFecther. + // But there are currently no such warnings returned in dataFecther.processContentType(); + // warning, + } = component; + + // if (warning && (mode === XP_REQUEST_MODE.EDIT || mode === XP_REQUEST_MODE.INLINE || mode === XP_REQUEST_MODE.ADMIN)) { + // return ( + // {warning} + // ); + // } + + const contentTypeDefinition = componentRegistry.getContentType<{ + componentRegistry: ComponentRegistry + }>(contentType); + if (!contentTypeDefinition) { + return ( + {`ContentType:${contentType} not registered in ComponentRegistry!`} + ); + } + + const {View: ContentTypeView} = contentTypeDefinition; + if (!ContentTypeView) { + return ( + {`No View found for contentType:${contentType} in ComponentRegistry!`} + ); + } + + if (!props) { + return ( + {`ContentType component missing props: ${contentType}!`} + ); + } + + return ( + + ); +}; diff --git a/src/ComponentRegistry/BaseLayout.tsx b/src/ComponentRegistry/BaseLayout.tsx new file mode 100644 index 0000000..179734a --- /dev/null +++ b/src/ComponentRegistry/BaseLayout.tsx @@ -0,0 +1,72 @@ +// import type {PartComponent} from '@enonic-types/core'; +import type { + ComponentRegistry, + RenderableLayoutComponent, +} from '../types'; + +import * as React from 'react'; +import { Message } from '../Common/Message'; +import { XP_REQUEST_MODE } from '../constants'; + +export function BaseLayout({ + component, + componentRegistry +}: { + component: RenderableLayoutComponent + componentRegistry: ComponentRegistry +}): JSX.Element { + const { + descriptor, + mode, + props, + warning, + } = component; + + const dataPortalComponentType = mode === XP_REQUEST_MODE.EDIT ? 'layout' : undefined; + + if (warning && (mode === XP_REQUEST_MODE.EDIT || mode === XP_REQUEST_MODE.INLINE || mode === XP_REQUEST_MODE.ADMIN)) { + return ( + {warning} + ); + } + + const layoutDefinition = componentRegistry.getLayout(descriptor); + if (!layoutDefinition) { + return ( + {`Layout descriptor:${descriptor} not registered in ComponentRegistry!`} + ); + } + + const {View: LayoutView} = layoutDefinition; + if (!LayoutView) { + return ( + {`No View found for layout descriptor:${descriptor} in ComponentRegistry!`} + ); + } + + if (!props) { + return ( + {`Layout component missing props: ${descriptor}!`} + ); + } + + return ( + + ); +} diff --git a/src/ComponentRegistry/BaseMacro.tsx b/src/ComponentRegistry/BaseMacro.tsx new file mode 100644 index 0000000..5ece726 --- /dev/null +++ b/src/ComponentRegistry/BaseMacro.tsx @@ -0,0 +1,39 @@ +import type { MacroComponentParams } from '../types'; + +import { Message } from '../Common/Message'; + +export function BaseMacro({ + children, + componentRegistry, + config, + descriptor, + mode, +}: MacroComponentParams) { + let msg = 'No Macro component provided to RichText.'; + + if (componentRegistry) { + const macroName = descriptor.includes(':') ? descriptor.split(':')[1] : descriptor; + + const MacroComponentDefinition = componentRegistry.getMacro<{ + children?: string | React.JSX.Element | React.JSX.Element[] + }>(macroName); + + if (MacroComponentDefinition) { + const MacroComponent = MacroComponentDefinition.View; + return ( + {children} + ); + } else { + msg = `Component Registry doesn't have a macro named: ${macroName}!`; + } + } + + return ( + + {msg} Can't render {descriptor}{" "} + with config {JSON.stringify(config, null, 4)} + + ); +} diff --git a/src/ComponentRegistry/BasePage.tsx b/src/ComponentRegistry/BasePage.tsx new file mode 100644 index 0000000..e26cac5 --- /dev/null +++ b/src/ComponentRegistry/BasePage.tsx @@ -0,0 +1,84 @@ +// import type {PartComponent} from '@enonic-types/core'; +import type { + ComponentRegistry, + RenderablePageComponent, +} from '../types'; + +import * as React from 'react'; +import { Message } from '../Common/Message'; +import { ErrorComponent } from '../Common/ErrorComponent'; +import { XP_REQUEST_MODE } from '../constants'; + +export function BasePage({ + component, + componentRegistry +}: { + component: RenderablePageComponent + componentRegistry: ComponentRegistry +}): JSX.Element { + const { + descriptor, + error, + mode, + props, + warning, + } = component; + + const dataPortalComponentType = mode === XP_REQUEST_MODE.EDIT ? 'page' : undefined; + + if (error && (mode === 'inline' || mode === 'preview')) { // In edit mode the error should be handeled by Content Studio. + return ( + + ); + } + + if (warning && (mode === XP_REQUEST_MODE.INLINE || mode === XP_REQUEST_MODE.EDIT || mode === XP_REQUEST_MODE.ADMIN)) { + return ( + + ); + } + + const pageDefinition = componentRegistry.getPage(descriptor); + if (!pageDefinition) { + return ( + {`Page descriptor:${descriptor} not registered in ComponentRegistry!`} + ); + } + + const {View: PageView} = pageDefinition; + if (!PageView) { + return ( + {`No View found for page descriptor:${descriptor} in ComponentRegistry!`} + ); + } + + if (!props) { + return ( + {`Page component missing props: ${descriptor}!`} + ); + } + + return ( + + ); +} diff --git a/src/ComponentRegistry/BasePart.tsx b/src/ComponentRegistry/BasePart.tsx new file mode 100644 index 0000000..187594c --- /dev/null +++ b/src/ComponentRegistry/BasePart.tsx @@ -0,0 +1,81 @@ +import type { + ComponentRegistry, + RenderablePartComponent, +} from '../types'; + +// import { toStr } from '@enonic/js-utils/value/toStr'; +import * as React from 'react'; +import { Message } from '../Common/Message'; +import { XP_REQUEST_MODE } from '../constants'; + +export function BasePart({ + component, + componentRegistry +}: { + component: RenderablePartComponent + componentRegistry: ComponentRegistry +}): JSX.Element { + + const { + descriptor, + mode, + props, + warning, + } = component; + + const dataPortalComponentType = mode === XP_REQUEST_MODE.EDIT ? 'part' : undefined; + + if (warning && (mode === XP_REQUEST_MODE.EDIT || mode === XP_REQUEST_MODE.INLINE || mode === XP_REQUEST_MODE.ADMIN)) { + return ( + + ); + } + + const partDefinition = componentRegistry.getPart<{ + componentRegistry: ComponentRegistry + }>(descriptor); + + if (!partDefinition) { + return ( + + ); + } + + const {View: PartView} = partDefinition; + if (!PartView) { + return ( + + ); + } + + if (!props) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/ComponentRegistry/BaseText.tsx b/src/ComponentRegistry/BaseText.tsx new file mode 100644 index 0000000..c9d26c4 --- /dev/null +++ b/src/ComponentRegistry/BaseText.tsx @@ -0,0 +1,50 @@ +import type { + ComponentRegistry, + RenderableTextComponent, + TextProps, +} from '../types'; + +import { toStr } from '@enonic/js-utils/value/toStr'; +import * as React from 'react'; +import { Message } from '../Common/Message'; +import { XpFallback } from './XpFallback'; +import { Text } from './Text'; +import { XP_REQUEST_MODE } from '../constants'; + +export const BaseText = ({ + component, + componentRegistry +}: { + component: RenderableTextComponent + componentRegistry: ComponentRegistry +}): JSX.Element => { + const { + mode, + props + } = component; + + const dataPortalComponentType = mode === XP_REQUEST_MODE.EDIT ? 'text' : undefined; + + if (!props) { + if (mode === XP_REQUEST_MODE.EDIT || mode === XP_REQUEST_MODE.INLINE || mode === XP_REQUEST_MODE.ADMIN) { + return ( + Text component missing props: {toStr(component)} + ); + } + console.warn('BaseText: Text component missing props:', toStr(component)); + return + } + + const textProps = props as TextProps; + + return ( + + ); +} diff --git a/src/ComponentRegistry/ComponentRegistry.tsx b/src/ComponentRegistry/ComponentRegistry.tsx new file mode 100644 index 0000000..d40ba15 --- /dev/null +++ b/src/ComponentRegistry/ComponentRegistry.tsx @@ -0,0 +1,76 @@ +import type { + ComponentDefinition, + ComponentDefinitionParams, + ComponentDictionary, + ComponentRegistry as ComponentRegistryInterface +} from '../types'; + + +export class ComponentRegistry implements ComponentRegistryInterface { + + private _contentTypes: ComponentDictionary = {}; + private _pages: ComponentDictionary = {}; + private _parts: ComponentDictionary = {}; + private _layouts: ComponentDictionary = {}; + private _macros: ComponentDictionary = {}; + + public addContentType(name: string, obj: ComponentDefinitionParams): void { + this._contentTypes[name] = obj as ComponentDefinition<{}>; + } + + public addMacro(name: string, obj: ComponentDefinitionParams): void { + this._macros[name] = obj as ComponentDefinition<{}>; + } + + public addLayout(name: string, obj: ComponentDefinitionParams): void { + this._layouts[name] = obj as ComponentDefinition<{}>; + } + + public addPage(name: string, obj: ComponentDefinitionParams): void { + this._pages[name] = obj as ComponentDefinition<{}>; + } + + public addPart(name: string, obj: ComponentDefinitionParams): void { + this._parts[name] = obj as ComponentDefinition<{}>; + } + + public getContentType(name: string): ComponentDefinition | undefined { + return this._contentTypes[name] as ComponentDefinition; + } + + public getLayout(name: string): ComponentDefinition | undefined { + return this._layouts[name] as ComponentDefinition; + } + + public getMacro(name: string): ComponentDefinition | undefined { + return this._macros[name] as ComponentDefinition; + } + + public getPage(name: string): ComponentDefinition | undefined { + return this._pages[name] as ComponentDefinition; + } + + public getPart(name: string): ComponentDefinition | undefined { + return this._parts[name] as ComponentDefinition; + } + + public hasContentType(name: string): boolean { + return this._contentTypes[name] !== undefined; + } + + public hasMacro(name: string): boolean { + return this._macros[name] !== undefined; + } + + public hasLayout(name: string): boolean { + return this._layouts[name] !== undefined; + } + + public hasPage(name: string): boolean { + return this._pages[name] !== undefined; + } + + public hasPart(name: string): boolean { + return this._parts[name] !== undefined; + } +} diff --git a/src/ComponentRegistry/Document.tsx b/src/ComponentRegistry/Document.tsx new file mode 100644 index 0000000..bd0f24b --- /dev/null +++ b/src/ComponentRegistry/Document.tsx @@ -0,0 +1,51 @@ +import type { + PageComponent, + // PageContributions, +} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from './ComponentRegistry'; + +import cx from 'clsx'; +import {Regions} from './Regions'; + +export function Document({ + bodyBegin, + bodyEnd, + className, + component, + componentRegistry, + headBegin, + headEnd, + title +}: { + bodyBegin?: React.ReactNode; + bodyEnd?: React.ReactNode; + className?: ClassValue + component: PageComponent + componentRegistry: ComponentRegistry + headBegin?: React.ReactNode; + headEnd?: React.ReactNode; + title?: string +}) { + const {regions} = component; + return ( + + + {headBegin ? headBegin : null} + {title ? {title} : null} + {headEnd ? headEnd : null} + + + {bodyBegin ? bodyBegin : null} + + {bodyEnd ? bodyEnd : null} + + + ); +} diff --git a/src/ComponentRegistry/Layout.tsx b/src/ComponentRegistry/Layout.tsx new file mode 100644 index 0000000..d648f21 --- /dev/null +++ b/src/ComponentRegistry/Layout.tsx @@ -0,0 +1,45 @@ +import type { + LiteralUnion, + Region +} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from './ComponentRegistry'; + +import cx from 'clsx'; +import {Regions} from './Regions'; + +export function Layout({ + as, + children, + className, + componentRegistry, + regions, + ...extraProps +}: Omit< + React.HTMLAttributes, + 'className' +> & { + as?: LiteralUnion; + children?: React.ReactNode + className?: ClassValue; + componentRegistry: ComponentRegistry; + regions: Record; +}) { + const ElementType = (as || 'div') as React.ElementType; + return ( + + {children} + + + ); +} diff --git a/src/ComponentRegistry/Page.tsx b/src/ComponentRegistry/Page.tsx new file mode 100644 index 0000000..9fdd150 --- /dev/null +++ b/src/ComponentRegistry/Page.tsx @@ -0,0 +1,45 @@ +import type { + LiteralUnion, + Region, +} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from './ComponentRegistry'; + +import cx from 'clsx'; +import {Regions} from './Regions'; + +export function Page({ + as, + children, + className, + componentRegistry, + regions, + ...extraProps +}: Omit< + React.HTMLAttributes, + 'className' +> & { + as?: LiteralUnion; + children?: React.ReactNode + className?: ClassValue + componentRegistry: ComponentRegistry + regions: Record; +}) { + const ElementType = (as || 'div') as React.ElementType; + return ( + + {children} + + + ); +} diff --git a/src/ComponentRegistry/Part.tsx b/src/ComponentRegistry/Part.tsx new file mode 100644 index 0000000..3508c8d --- /dev/null +++ b/src/ComponentRegistry/Part.tsx @@ -0,0 +1,32 @@ +import type {LiteralUnion} from '@enonic-types/core'; +import type {ClassValue} from 'clsx'; + +import cx from 'clsx'; + +export function Part({ + as, + children, + className, + ...extraProps +}: Omit< + React.HTMLAttributes, + 'className' +> & { + as?: LiteralUnion; + children?: React.ReactNode + className?: ClassValue +}) { + const ElementType = (as || 'div') as React.ElementType; + return ( + + {children} + + ); +} diff --git a/src/ComponentRegistry/Region.tsx b/src/ComponentRegistry/Region.tsx new file mode 100644 index 0000000..665b7e7 --- /dev/null +++ b/src/ComponentRegistry/Region.tsx @@ -0,0 +1,46 @@ +import type {ClassValue} from 'clsx'; +import type {ComponentRegistry} from './ComponentRegistry'; +import type {RenderableComponent} from '../types'; + +import cx from 'clsx'; +import {BaseComponent} from './BaseComponent'; + + +export interface RegionProps { + as?: string + className?: ClassValue + components: RenderableComponent[] + componentRegistry: ComponentRegistry + name: string +} + + +export const Region = ({ + as, + className, + components = [], + componentRegistry, + name, +}: RegionProps) => { + if (!((name || '').trim())) { + console.error(` name: ${JSON.stringify(name)}`); + throw Error(`Can't render without a 'name' prop.`); + } + // console.debug('Region name:', name); + + const ElementType = (as || 'div') as keyof JSX.IntrinsicElements; + return ( + + { + components.map((component, i) => ) + } + + ); +} diff --git a/src/ComponentRegistry/Regions.tsx b/src/ComponentRegistry/Regions.tsx new file mode 100644 index 0000000..da024f9 --- /dev/null +++ b/src/ComponentRegistry/Regions.tsx @@ -0,0 +1,25 @@ +import type {Region as RegionType} from '@enonic-types/core'; +import type {ComponentRegistry} from './ComponentRegistry'; +import type {RenderableComponent} from '../types'; + +import {Region} from './Region'; + +export interface RegionsProps { + componentRegistry: ComponentRegistry; + regions: Record; +} + +export function Regions({ + componentRegistry, + regions +}: RegionsProps) { + // console.debug('Regions regions:', regions); + return Object.keys(regions).map((name, i) => + + ) +} diff --git a/src/ComponentRegistry/Text.tsx b/src/ComponentRegistry/Text.tsx new file mode 100644 index 0000000..d460613 --- /dev/null +++ b/src/ComponentRegistry/Text.tsx @@ -0,0 +1,32 @@ +import type {TextProps} from '../types'; + +import cx from 'clsx'; +import {RichText} from '../RichText/RichText'; + + +export function Text({ + as, + className, + componentRegistry, + data, + mode, + ...extraProps +}: TextProps): JSX.Element { + const ElementType = (as || 'div') as React.ElementType; + return ( + + + + ); +} diff --git a/src/ComponentRegistry/XpFallback.tsx b/src/ComponentRegistry/XpFallback.tsx new file mode 100644 index 0000000..4f9954f --- /dev/null +++ b/src/ComponentRegistry/XpFallback.tsx @@ -0,0 +1,16 @@ +import type {Component} from '@enonic-types/core'; +import { HtmlComment } from '../Common/HtmlComment'; + +export function XpFallback({ + component +}: { + component?: Component +}): JSX.Element | null { + if (!component || !component.path) { + return null; + } + return ( + + ); +} + diff --git a/src/ComponentTag.ts b/src/ComponentTag.ts deleted file mode 100644 index fd67f29..0000000 --- a/src/ComponentTag.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * THIS IS NOT A REACT COMPONENT, just a function that returns an HTML string: a tag with a component path reference. - * XP picks this up from the controller's returned response.body and inserts the inserted XP component with that path. - * If you want to use this directly, this means two things: - * - In order to use this function in react4xp, the parent entry (or react component) must treat it as a pure-text function - * and insert the text with a 'dangerouslySetInnerHTML' prop. - * - The resulting react component MUST be server-side rendered (for now at least - this may change in later versions)! - * - * @param {Object} component - The component data object (e.g. an item in the array content.page.regions.main.components). Must - * have a 'path' string attribute. - */ -import type {Component} from '@enonic-types/core'; - -export default (component: Component) => (component && component.path) ? - `\t\t\t\t\t\t\n\t\t\t\t\t\t\t` : - null; diff --git a/src/Layout.tsx b/src/Layout.tsx deleted file mode 100644 index 97ba2d8..0000000 --- a/src/Layout.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type {Region as RegionType} from '@enonic-types/core'; - -import PropTypes from 'prop-types'; - -import Regions from './Regions'; - -/** - * Layout controller template: wraps a react rendering of a bare-bones XP layout, with regions if supplied with regions data. - * Can be used as a wrapping component, nesting regular react children:

A layout!

- * - * @param {Object} [regionsData] - (optional although layouts make little sense without regions): regions data object (e.g. component.regions). - * Keys are region names, values are region data. - * @param {(string|string[])} [regionNames] - selects to display only one, or some specific, of the available regions in the - * regions data. The array defines sequence, so this can also be used to display of all regions in a specific order. - * If omitted, all regions are displayed in the order of Object.keys(component.regions). - * @param {(boolean|string|Object)} [regionClasses] - HTML class for the region elements, added after "xp-region". - * If boolean, and it's true: adds a class that is the same as the name - * If string, all regions get that class. - * If object: keys are region names and values are a class name string for each region. - * @param {string} [containerTag] - the HTML tag of the layout's outer container element. Defaults to 'div'. - * @param {string} [containerClass] - the HTML class of the layout's outer container element. No default. - * @param {React.ReactNode} children - nested react components - * @param {boolean} [childrenAfterRegions] - if false or omitted, children will be rendered first in body. - * If true, they will be rendered below the regions. - * @returns A react4xp-representation (react component) of an XP layout. Must be SERVER-SIDE-rendered by react4xp! - */ -const Layout = ({ - regionsData, - regionNames, - regionClasses, - containerTag, - containerClass, - children, - childrenAfterRegions, -}: { - regionsData?: Record - regionNames?: string | string[] - regionClasses?: boolean | string | Record - containerTag?: string - containerClass?: string - children?: React.ReactNode - childrenAfterRegions?: boolean -}): React.JSX.Element => { - const TAG = (containerTag || 'div') as keyof JSX.IntrinsicElements; - - return - {!childrenAfterRegions ? children : null} - {regionsData ? : null} - {childrenAfterRegions ? children : null} - ; -}; - - -Layout.propTypes = { - regionsData: PropTypes.objectOf( - PropTypes.shape({ - components: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string.isRequired, - }) - ), - }) - ), - regionNames: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - regionClasses: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.objectOf(PropTypes.string), - ]), - containerTag: PropTypes.string, - containerClass: PropTypes.string, - childrenAfterRegions: PropTypes.bool, -}; - -export default Layout; diff --git a/src/Page.tsx b/src/Page.tsx deleted file mode 100644 index 977d0f5..0000000 --- a/src/Page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type {Region as RegionType} from '@enonic-types/core'; - -import PropTypes from 'prop-types'; - -import Regions from './Regions'; - -/** - * Page controller template: wraps a react rendering of a bare-bones HTML page, with regions if supplied with regions data. - * Can be used as a wrapping component, neating regular react children:

A heading!

- * - * @param {string} [title] - Page title string - * @param {Object} [regionsData] - regions data object (e.g. content.page.regions). - * Keys are region names, values are region data. - * @param {(string|string[])} [regionNames] - selects to display only one, or some specific, of the available regions in the - * regions data. The array defines sequence, so this can also be used to display of all regions in a specific order. - * If omitted, all regions are displayed in the order of Object.keys(regionsData). - * @param {(boolean|string|Object)} [regionClasses] - HTML class for the region elements, added after "xp-region". - * If boolean, and it's true: adds a class that is the same as the name - * If string, all regions get that class. - * If object: keys are region names and values are a class name string for each region. - * @param {React.ReactNode} [children] - nested react components - * @param {boolean} [childrenAfterRegions] - if false or omitted, children will be rendered first in body. - * If true, they will be rendered below the regions. - * @returns React component array with and , but NOT ! This is left up to the calling - * context. - */ -const Page = ({ - title, - regionsData, - regionNames, - regionClasses, - children, - childrenAfterRegions -}: { - title?: string - regionsData?: Record - regionNames?: string | string[] - regionClasses?: boolean | string | Record - children?: React.ReactNode - childrenAfterRegions?: boolean -}): React.JSX.Element[] => { - return [ - - {title ? {title} : null} - , - - {!childrenAfterRegions ? children : null} - {regionsData ? : null} - {childrenAfterRegions ? children : null} - , - ]; -}; - - -Page.propTypes = { - title: PropTypes.string, - regionsData: PropTypes.objectOf( - PropTypes.shape({ - components: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string.isRequired, - }) - ), - }) - ), - regionNames: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - regionClasses: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.objectOf(PropTypes.string), - ]), - childrenAfterRegions: PropTypes.bool, -}; - -export default Page; diff --git a/src/Region.tsx b/src/Region.tsx deleted file mode 100644 index 228f9c5..0000000 --- a/src/Region.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import type {Region as RegionType} from '@enonic-types/core'; - -import PropTypes from 'prop-types'; - -import ComponentTag from './ComponentTag'; - -/** - * @param {string} name - Region name, as defined in a part's/page's/layout's XML definition - * @param {Object} regionData - data object for this specific region, from part or page or layout data - * (e.g. for the 'main' region in a page, regionData could be: content.page.regions.main) - * @param {string} [tag] - Sets the HTML tag for the region. If omitted, "div" is the default. - * @param {string} [addClass] - Adds an HTML class for the region, after "xp-region". - * @returns A react4xp-representation (react component) of an XP region. Must be SERVER-SIDE-rendered by react4xp! - */ -const Region = ({ - name, - regionData, - tag, - addClass -}: { - name: string - regionData: RegionType - tag?: string - addClass?: string -}) => { - if (!((name || '').trim())) { - console.error(` name: ${JSON.stringify(name)}`); - throw Error(`Can't render without a 'name' prop.`); - } - - if ( - !regionData || - typeof regionData !== 'object' || - !Object.keys(regionData).length - ) { - console.error(` regionData: ${JSON.stringify(regionData)}`); - throw Error(`Can't render without a 'regionData' prop.`); - } - - const TAG = (tag || "div") as keyof JSX.IntrinsicElements; - return 0 ? - regionData.components - .map(component => ComponentTag(component)) - .join('\n') : - '' - }\t\t\t\t\t\n`, - }} - > as React.JSX.Element; -}; -Region.propTypes = { - name: PropTypes.string.isRequired, - regionData: PropTypes.shape({ - components: PropTypes.arrayOf(PropTypes.shape({ - path: PropTypes.string.isRequired, - })), - }).isRequired, - tag: PropTypes.string, - addClass: PropTypes.string, -}; - -export default Region; diff --git a/src/Regions.tsx b/src/Regions.tsx deleted file mode 100644 index 7ba63d5..0000000 --- a/src/Regions.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type {Region as RegionType} from '@enonic-types/core'; - -import PropTypes from 'prop-types'; - -import Region from './Region'; - -/** - * @param {Object} regionsData - regions data object (e.g. content.page.regions). - * Keys are region names, values are region data. - * @param {(string|string[])} [names] - selects to display only one, or some specific, of the available regions in the - * regions data. The array defines sequence, so this can also be used to display of all regions in a specific order. - * If omitted, all regions are displayed in the order of Object.keys(regionsData). - * @param {(string|Object)} [tags] - HTML tag for the region elements. - * If string, all regions get that tag. - * If object: keys are region names and values are an HTML tag string for each region. - * @param {(boolean|string|Object)} [classes] - HTML class for each region element, added after "xp-region". - * If boolean, and it's true: adds a class that is the same as the name of the region - * If string, all regions get that same class. - * If object: keys are region names, values are the class name string for that region. - * @returns {Region[]} An array of elements. - */ -const Regions = ({ - regionsData, - names, - tags, - classes -}: { - regionsData: Record - names?: string | string[] - tags?: string | Record - classes?: boolean | string | Record -}): React.JSX.Element[] => { - if ( - !regionsData || - typeof regionsData !== 'object' - ) { - console.error(' regions: ' + JSON.stringify(regionsData)); - throw Error("Can't render without a 'regionsData' prop."); - } - - const selectedRegions = - !names ? Object.keys(regionsData) : - (typeof names === 'string') ? [names] : - Array.isArray(names) ? names : null; - - if (!selectedRegions) { - console.error(' names: ' + JSON.stringify(names)); - throw Error("Can't render : 'names' prop must be a string, an array of strings or omitted/falsy."); - } - - // TODO: sanitize tag and name: not all characters (or tags) are acceptable - return selectedRegions.map(name => - //@ts-ignore - - ); -}; -Regions.propTypes = { - regionsData: PropTypes.objectOf( - PropTypes.shape({ - components: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string.isRequired, - }) - ), - }) - ).isRequired, - names: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.arrayOf( - PropTypes.string - ), - ]), - tags: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.objectOf( - PropTypes.string - ), - ]), - classes: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.objectOf( - PropTypes.string - ), - ]), -}; - -export default Regions; diff --git a/src/RichText/ErrorBoundary/ErrorBoundaryClient.tsx b/src/RichText/ErrorBoundary/ErrorBoundaryClient.tsx index 963ce5f..d0e99be 100644 --- a/src/RichText/ErrorBoundary/ErrorBoundaryClient.tsx +++ b/src/RichText/ErrorBoundary/ErrorBoundaryClient.tsx @@ -1,7 +1,12 @@ 'use client'; +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; + import React, {Component, ReactNode} from 'react'; -import {ErrorComponent} from '../ErrorComponent'; +import {Message} from '../../Common/Message'; interface ErrorBoundaryProps { children: ReactNode @@ -40,9 +45,15 @@ class ErrorBoundary extends Component { } } -export function ErrorBoundaryClient({children}: { children: React.ReactNode }) { +export function ErrorBoundaryClient({ + children, + mode, +}: { + children: React.ReactNode + mode?: LiteralUnion +}) { return ( - {error.message}}> + {error.message}}> {children} ); diff --git a/src/RichText/ErrorBoundary/ErrorBoundaryServer.tsx b/src/RichText/ErrorBoundary/ErrorBoundaryServer.tsx index adb56b7..5ce3370 100644 --- a/src/RichText/ErrorBoundary/ErrorBoundaryServer.tsx +++ b/src/RichText/ErrorBoundary/ErrorBoundaryServer.tsx @@ -1,6 +1,32 @@ -import React from 'react'; +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; +import * as React from 'react'; +import { Message } from '../../Common/Message'; +import { XP_REQUEST_MODE } from '../../constants'; // ErrorBoundaries are not supported on server in Next.js -export function ErrorBoundaryServer({children}: { children: React.ReactNode }) { - return <>{children}; +export function ErrorBoundaryServer({ + children, + mode, +}: { + children: React.ReactNode + mode?: LiteralUnion +}) { + try { + return <>{children}; + } catch (e) { + return ( + +

Error rendering component

+

{e.message}

+ { + mode === XP_REQUEST_MODE.EDIT && ( +
{e.stack || ''}
+ ) + } +
+ ); + } } diff --git a/src/RichText/ErrorBoundary/ErrorBoundaryWrapper.tsx b/src/RichText/ErrorBoundary/ErrorBoundaryWrapper.tsx index 06d3a9f..67fad25 100644 --- a/src/RichText/ErrorBoundary/ErrorBoundaryWrapper.tsx +++ b/src/RichText/ErrorBoundary/ErrorBoundaryWrapper.tsx @@ -1,12 +1,22 @@ -import React from 'react'; +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; +import * as React from 'react'; -export function ErrorBoundaryWrapper({children}: { children: React.ReactNode }) { +export function ErrorBoundaryWrapper({ + children, + mode, +}: { + children: React.ReactNode + mode?: LiteralUnion +}) { if (typeof window === 'undefined') { const {ErrorBoundaryServer} = require('./ErrorBoundaryServer'); - return {children}; + return {children}; } else { const {ErrorBoundaryClient} = require('./ErrorBoundaryClient'); - return {children}; + return {children}; } } diff --git a/src/RichText/ErrorComponent.tsx b/src/RichText/ErrorComponent.tsx deleted file mode 100644 index 2bcf399..0000000 --- a/src/RichText/ErrorComponent.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type {ReactNode} from 'react'; - - -export function ErrorComponent({ - children, -}: { - children: ReactNode -}) { - return
{children}
-} diff --git a/src/RichText/Macro.tsx b/src/RichText/Macro.tsx index cf008cd..1f89f17 100644 --- a/src/RichText/Macro.tsx +++ b/src/RichText/Macro.tsx @@ -1,12 +1,19 @@ -import type {MacroComponentParams} from '../types'; +import type { MacroComponentParams } from '../types'; + +import { Message } from '../Common/Message'; export function Macro({ - config, - descriptor - }: MacroComponentParams) { - // throw new Error(`Macro not found: ${descriptor}`); - return
No Macro component provided to RichText. Can't render {descriptor} with config {JSON.stringify(config, null, 4)}
+ config, + descriptor, + mode, +}: MacroComponentParams) { + const msg = 'No Macro component provided to RichText.'; + return ( + + {msg} Can't render {descriptor}{" "} + with config {JSON.stringify(config, null, 4)} + + ); } diff --git a/src/RichText.tsx b/src/RichText/RichText.tsx similarity index 60% rename from src/RichText.tsx rename to src/RichText/RichText.tsx index 5e37d62..b4e9c76 100644 --- a/src/RichText.tsx +++ b/src/RichText/RichText.tsx @@ -1,27 +1,31 @@ -import type {RichTextParams} from './types' +import type {RichTextParams} from '../types' // Converts an HTML string to one or more React elements import * as parser from 'html-react-parser'; +import * as React from 'react'; // Replaces "matching" domNodes -import {createReplacer} from './RichText/createReplacer'; +import {createReplacer} from './createReplacer'; -import {Image as ImageFallback} from './RichText/Image'; -import {Link as LinkFallback} from './RichText/Link'; -import {Macro as MacroFallback} from './RichText/Macro'; +import {Image as ImageFallback} from './Image'; +import {Link as LinkFallback} from './Link'; +import { BaseMacro } from '../ComponentRegistry/BaseMacro'; export function RichText>({ className, + componentRegistry, data, Image = ImageFallback, Link = LinkFallback, - Macro = MacroFallback, + Macro = BaseMacro, + mode, replacer, tag, - ...rest + ...restProps }: RichTextParams) { + // console.info('RichText', {data, Macro, tag, ...restProps}); const CustomTag = tag as keyof JSX.IntrinsicElements || 'section'; return { @@ -29,13 +33,15 @@ export function RichText>({ /* try parser.default.default first because import is wrapped with __toesm() in cjs files * for node compatibility, which adds default export resulting in parser.default.default */ ? (((parser.default as any).default as typeof parser.default) || parser.default)(data.processedHtml, { - replace: createReplacer({ - ...rest, - // These should be last, so they can't be overridden + replace: createReplacer({ + ...restProps, + // These should be last, so they can't be overridden: + componentRegistry, data, Image, Link, Macro, + mode, replacer, }), }) diff --git a/src/RichText/createReplacer.ts b/src/RichText/createReplacer.ts index ee47894..9d65e5e 100644 --- a/src/RichText/createReplacer.ts +++ b/src/RichText/createReplacer.ts @@ -1,5 +1,5 @@ import type {DOMNode} from 'html-react-parser'; -import type {ImageComponent, LinkComponent, MacroComponent, Replacer, ReplacerResult, RichTextData} from '../types'; +import type {CreateReplacerParams, ReplacerResult} from '../types'; import {ElementType} from 'domelementtype'; @@ -12,19 +12,15 @@ import {replaceMacro} from './replaceMacro'; // Replaces "matching" domNodes export function createReplacer>({ + componentRegistry, data, Image, Link, Macro, + mode, replacer, - ...rest -}: { - data: RichTextData - Image: ImageComponent - Link: LinkComponent - Macro: MacroComponent - replacer?: Replacer -}): (domNode: DOMNode) => ReplacerResult { + ...restProps +}: CreateReplacerParams): (domNode: DOMNode) => ReplacerResult { const { images } = data; @@ -37,41 +33,46 @@ export function createReplacer>({ switch (el.tagName) { case IMG_TAG: return replaceImage({ - ...rest, + ...restProps, el, Image, - images + images, + mode, }); case LINK_TAG: return replaceLink({ - ...rest, - // These should be last, so they can't be overridden + ...restProps, + // These should be last, so they can't be overridden: createReplacer, data, el, Image, Link, Macro, + mode, replacer, }); case MACRO_TAG: - return replaceMacro({ - ...rest, - // These should be last, so they can't be overridden + return replaceMacro({ + ...restProps, + // These should be last, so they can't be overridden: + componentRegistry, createReplacer, data, el, Image, Link, Macro, + mode, replacer, }); default: if (replacer) { - const result = replacer( + const result = replacer({ el, data, - ); + mode + }); if (result) { return result; } diff --git a/src/RichText/cssToReactStyle.ts b/src/RichText/cssToReactStyle.ts index 4df54af..b8bec5e 100644 --- a/src/RichText/cssToReactStyle.ts +++ b/src/RichText/cssToReactStyle.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import * as React from 'react'; export function cssToReactStyle(cssString?: string): React.CSSProperties { diff --git a/src/RichText/replaceImage.tsx b/src/RichText/replaceImage.tsx index 2f8c6c1..dc79e82 100644 --- a/src/RichText/replaceImage.tsx +++ b/src/RichText/replaceImage.tsx @@ -1,10 +1,14 @@ +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; import type {Element} from 'domhandler'; import type {ImageComponent, ImageData, ImageComponentParams} from '../types'; import {IMG_ATTR} from '../constants'; import {cssToReactStyle} from './cssToReactStyle'; -import {ErrorComponent} from './ErrorComponent'; +import {ErrorComponent} from '../Common/ErrorComponent'; import {ErrorBoundaryWrapper} from './ErrorBoundary/ErrorBoundaryWrapper'; @@ -12,11 +16,13 @@ export function replaceImage>({ el, Image, images, - ...rest + mode, + ...restProps }: { el: Element Image: ImageComponent images?: ImageData[] + mode?: LiteralUnion }) { if (!images || !images.length) { return Can't replace image, when there are no images in the data object! @@ -49,9 +55,9 @@ export function replaceImage>({ style: imageStyle } = imageData; - const imgProps = {...rest, alt, image, imageStyle, sizes, src, srcSet, style} as ImageComponentParams; + const imgProps = {...restProps, alt, image, imageStyle, sizes, src, srcSet, style} as ImageComponentParams; - return + return ; } diff --git a/src/RichText/replaceLink.tsx b/src/RichText/replaceLink.tsx index f108b43..8ccf557 100644 --- a/src/RichText/replaceLink.tsx +++ b/src/RichText/replaceLink.tsx @@ -1,10 +1,15 @@ +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; import type {Element} from 'domhandler'; import type {DOMNode} from 'html-react-parser'; -import {domToReact} from 'html-react-parser'; import type {LinkComponent, ImageComponent, MacroComponent, Replacer, RichTextData, LinkComponentParams} from '../types'; import type {createReplacer as CreateReplacer} from './createReplacer'; + +import * as htmlReactParser from 'html-react-parser'; import {LINK_ATTR} from '../constants'; -import {ErrorComponent} from './ErrorComponent'; +import {ErrorComponent} from '../Common/ErrorComponent'; import {ErrorBoundaryWrapper} from './ErrorBoundary/ErrorBoundaryWrapper'; @@ -15,8 +20,9 @@ export function replaceLink>({ Image, Link, Macro, + mode, replacer, - ...rest + ...restProps }: { createReplacer: typeof CreateReplacer data: RichTextData @@ -24,6 +30,7 @@ export function replaceLink>({ Image: ImageComponent, Link: LinkComponent Macro: MacroComponent + mode?: LiteralUnion replacer?: Replacer }) { const { @@ -61,21 +68,30 @@ export function replaceLink>({ uri } = linkData; - const linkProps = {...rest, content, href, media, target, title, uri} as LinkComponentParams; + const linkProps = { + ...restProps, + content, + href, + media, + target, + title, + uri + } as LinkComponentParams; - const children = domToReact(el.children as DOMNode[], { + const children = htmlReactParser.domToReact(el.children as DOMNode[], { replace: createReplacer({ - ...rest, + ...restProps, // These should be last, so they can't be overridden data, Image, Link, Macro, + mode, replacer }) }) - return + return {children} ; } diff --git a/src/RichText/replaceMacro.tsx b/src/RichText/replaceMacro.tsx index 0b5370c..0b47529 100644 --- a/src/RichText/replaceMacro.tsx +++ b/src/RichText/replaceMacro.tsx @@ -1,33 +1,28 @@ -import type {Element} from 'domhandler'; -import type {MacroComponent, MacroComponentParams, RichTextData, ImageComponent, LinkComponent, Replacer} from '../types'; +// import type {Element} from 'domhandler'; +import type {DOMNode} from 'html-react-parser'; +import type {MacroComponentParams, ReplaceMacroParams} from '../types'; import {MACRO_ATTR} from '../constants'; -import {ErrorComponent} from './ErrorComponent'; -import {domToReact, type DOMNode} from 'html-react-parser'; -import {type createReplacer as CreateReplacer} from './createReplacer'; +import {ErrorComponent} from '../Common/ErrorComponent'; +import * as htmlReactParser from 'html-react-parser'; +// import {type createReplacer as CreateReplacer} from './createReplacer'; import {ErrorBoundaryWrapper} from './ErrorBoundary/ErrorBoundaryWrapper'; import {sanitizeGraphqlName} from '../utils/sanitizeGraphqlName'; export function replaceMacro>({ + componentRegistry, createReplacer, data, el, Image, Link, Macro, + mode, replacer, - ...rest -}: { - createReplacer: typeof CreateReplacer - data: RichTextData - el: Element - Image: ImageComponent, - Link: LinkComponent - Macro: MacroComponent - replacer?: Replacer -}) { + ...restProps +}: ReplaceMacroParams) { const ref = el.attribs[MACRO_ATTR]; if (!ref) { return Macro element has no data-macro-ref attribute! @@ -49,21 +44,40 @@ export function replaceMacro>({ const {descriptor, name, config: configs} = macroData; const config = configs[sanitizeGraphqlName(name)]; + if (componentRegistry) { + const MacroComponentDefinition = componentRegistry.getMacro(name); + if (MacroComponentDefinition) { + const MacroComponent = MacroComponentDefinition.View; + return ( + + ); + } + } + // config and descriptor should be last, so they can't be overridden - const props = {...rest, config, descriptor} as MacroComponentParams; + const props = { + ...restProps, + componentRegistry, + config, + descriptor, + mode + } as MacroComponentParams; - const children = domToReact(el.children as DOMNode[], { + const children = htmlReactParser.domToReact(el.children as DOMNode[], { replace: createReplacer({ - ...rest, // These should be last, so they can't be overridden + ...restProps, + // These should be last, so they can't be overridden: + componentRegistry, data, Image, Link, Macro, + mode, replacer }) }); - return + return {children} ; } diff --git a/src/constants.ts b/src/constants.ts index 0fab9f7..76eae78 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import type { CSSProperties } from 'react'; + export const IMG_TAG = 'img'; export const LINK_TAG = 'a'; export const MACRO_TAG = 'editor-macro'; @@ -5,3 +7,48 @@ export const MACRO_TAG = 'editor-macro'; export const IMG_ATTR = 'data-image-ref'; export const LINK_ATTR = 'data-link-ref'; export const MACRO_ATTR = 'data-macro-ref'; + +export enum RENDERABLE_COMPONENT_TYPE { + CONTENT_TYPE = 'contentType', + ERROR = 'error', + FRAGMENT = 'fragment', + LAYOUT = 'layout', + PAGE = 'page', + PART = 'part', + TEXT = 'text', + WARNING = 'warning', +} + +export enum XP_REQUEST_MODE { + ADMIN = 'admin', + EDIT = 'edit', + INLINE = 'inline', + LIVE = 'live', + PREVIEW = 'preview', +} + +export const CONTENT_STUDIO_EDIT_MODE_PLACEHOLDER_STYLE: CSSProperties = { + backgroundColor: '#ffffff', + borderWidth: '2px', + borderStyle: 'dashed', + borderRadius: '4px', + boxSizing: 'border-box', + display: 'block', + fontFamily: 'Open Sans, Helvetica, sans-serif', + fontSize: '20px', + lineHeight: '33px', + margin: '1px 0 10px', + minHeight: '137px', + padding: '50px 15px', + position: 'relative', + textAlign: 'center', +} + +const WARNING_BORDER_COLOR = '#c9ba9b'; // light yellow +const WARNING_COLOR = '#794b02'; // brown + +export const WARNING_STYLE = { + ...CONTENT_STUDIO_EDIT_MODE_PLACEHOLDER_STYLE, + borderColor: WARNING_BORDER_COLOR, + color: WARNING_COLOR, +}; diff --git a/src/index.ts b/src/index.ts index c141712..e7ea214 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,16 @@ +// WARNING: This file should NOT be imported into server-side Nashorn code. +// It is supoosed to be used in React4XP (GraalJS) and Next.js projects. +// The reason is that Nashorn does NOT support Uint16Array: +// * which is used in the 'entities' node module, +// * which is used by the 'html-react-parser' node module, +// * which is used by RichText. export type { - Content, ContentUri, + RenderableComponent, + RenderableLayoutComponent, + RenderablePageComponent, + RenderablePartComponent, + RenderableTextComponent, ImageComponent, ImageComponentParams, ImageContent, @@ -18,23 +28,37 @@ export type { MediaUri, Replacer, ReplacerResult, + RichtextContent, RichTextData, RichTextParams, } from './types'; -import Layout from './Layout' -import Page from './Page' -import ComponentTag from './ComponentTag'; -import Region from './Region'; -import Regions from './Regions'; +export { ErrorComponent } from './Common/ErrorComponent'; +export { HtmlComment } from './Common/HtmlComment'; +export { Warning } from './Common/Warning'; -export { - ComponentTag, - Layout, - Page, - Region, - Regions, -}; -export { RichText } from './RichText'; +export { BaseComponent } from './ComponentRegistry/BaseComponent'; +export { BaseContentType } from './ComponentRegistry/BaseContentType'; +export { BaseLayout } from './ComponentRegistry/BaseLayout'; +export { BaseMacro } from './ComponentRegistry/BaseMacro'; +export { BasePage } from './ComponentRegistry/BasePage'; +export { BasePart } from './ComponentRegistry/BasePart'; +export { BaseText } from './ComponentRegistry/BaseText'; + +// NOTE: This is the implementation, not the type, causes "--jsx is not set" warnings in other projects when imported as a type. +export { ComponentRegistry } from './ComponentRegistry/ComponentRegistry'; + +export { Layout } from './ComponentRegistry/Layout'; +export { Page } from './ComponentRegistry/Page'; +export { Part } from './ComponentRegistry/Part'; +export { Region } from './ComponentRegistry/Region'; +export { Regions } from './ComponentRegistry/Regions'; +export { Text } from './ComponentRegistry/Text'; +export { XpFallback } from './ComponentRegistry/XpFallback'; + +export { Image } from './RichText/Image'; +export { Link } from './RichText/Link'; +export { Macro } from './RichText/Macro'; +export { RichText } from './RichText/RichText'; export { cssToReactStyle } from './RichText/cssToReactStyle'; -export {sanitizeGraphqlName} from './utils/sanitizeGraphqlName'; +export { sanitizeGraphqlName } from './utils/sanitizeGraphqlName'; diff --git a/src/nashorn.ts b/src/nashorn.ts new file mode 100644 index 0000000..fbd281e --- /dev/null +++ b/src/nashorn.ts @@ -0,0 +1,15 @@ +export { + RenderableComponent, + RenderableContentType, + RenderableError, + RenderableLayoutComponent, + RenderablePageComponent, + RenderablePartComponent, + RenderableTextComponent, + RenderableWarning, + XpRunMode, +} from './types/Renderable'; +export { + RENDERABLE_COMPONENT_TYPE, + XP_REQUEST_MODE, +} from './constants'; diff --git a/src/types/ComponentRegistry.ts b/src/types/ComponentRegistry.ts new file mode 100644 index 0000000..cf7ecba --- /dev/null +++ b/src/types/ComponentRegistry.ts @@ -0,0 +1,27 @@ +export interface ComponentDefinitionParams { + View: React.FunctionComponent +} + +export interface ComponentDefinition { + View: React.FunctionComponent +} + +export type ComponentDictionary = Record>; + +export interface ComponentRegistry { + addContentType(name: string, obj: ComponentDefinitionParams): void + addMacro(name: string, obj: ComponentDefinitionParams): void + addLayout(name: string, obj: ComponentDefinitionParams): void + addPage(name: string, obj: ComponentDefinitionParams): void + addPart(name: string, obj: ComponentDefinitionParams): void + getContentType(name: string): ComponentDefinition | undefined + getMacro(name: string): ComponentDefinition | undefined + getLayout(name: string): ComponentDefinition | undefined + getPage(name: string): ComponentDefinition | undefined + getPart(name: string): ComponentDefinition | undefined + hasContentType(name: string): boolean + hasMacro(name: string): boolean + hasLayout(name: string): boolean + hasPage(name: string): boolean + hasPart(name: string): boolean +} diff --git a/src/types/Renderable.ts b/src/types/Renderable.ts new file mode 100644 index 0000000..48199fd --- /dev/null +++ b/src/types/Renderable.ts @@ -0,0 +1,87 @@ +// There is a difference between the core enonic types and what Guillotine returns: +import type { + ComponentDescriptor, + // LayoutComponent, + LiteralUnion, + // PageComponent, + RequestMode, + TextComponent, +} from '@enonic-types/core'; +import type { TextBaseProps } from './TextBaseProps'; + +export type XpRunMode = 'development' | 'production'; + +export interface RenderableContentType { + contentType: string; + mode: LiteralUnion; + props?: Record + type: 'contentType'; +} + +export interface RenderableError { + html: string; + mode: LiteralUnion; + path: string; + type: 'error'; +} + +export interface RenderableRegion { + name: string; + components: RenderableComponent[]; +} + +export type RenderableRegions = Record; + +export interface RenderableLayoutComponent { + // config: never + descriptor: ComponentDescriptor; + mode: LiteralUnion; + path?: string // Missing in fragmentPreview https://github.com/enonic/xp/issues/10116 + props?: Record + regions: RenderableRegions; + type: 'layout' + warning?: string; +} + +export interface RenderablePageComponent { + // config: never; + descriptor: ComponentDescriptor; + error?: string; + mode: LiteralUnion; + path: '/'; + props?: Record; + regions: RenderableRegions; + type: 'page'; + warning?: string; +} + +export interface RenderablePartComponent { + // config: never + descriptor: ComponentDescriptor; + mode: LiteralUnion; + path?: string // Missing in fragmentPreview https://github.com/enonic/xp/issues/10116 + props?: Record + type: 'part' + warning?: string; +} + +export interface RenderableWarning { + html: string; + mode: LiteralUnion; + path: string; + type: 'warning'; +} + +export type RenderableTextComponent = TextComponent & { + mode: LiteralUnion; + props?: TextBaseProps; +} + +export type RenderableComponent = + | RenderableContentType + | RenderableError + | RenderableLayoutComponent + | RenderablePageComponent + | RenderablePartComponent + | RenderableTextComponent + | RenderableWarning; diff --git a/src/types/RichText.ts b/src/types/RichText.ts new file mode 100644 index 0000000..6447e85 --- /dev/null +++ b/src/types/RichText.ts @@ -0,0 +1,191 @@ +import type { + LiteralUnion, + RequestMode, +} from '@enonic-types/core'; +import type { + DOMNode, + Element +} from 'html-react-parser'; +import type {ReactNode, JSX} from 'react'; +import type {ComponentRegistry} from './ComponentRegistry'; + +export type CreateReplacer> = (params: CreateReplacerParams) => (domNode: DOMNode) => ReplacerResult; + +export interface CreateReplacerParams> { + componentRegistry?: ComponentRegistry; + data: RichTextData; + Image: ImageComponent; + Link: LinkComponent; + Macro: MacroComponent; + mode?: LiteralUnion; + replacer?: Replacer; +} + +export type ImageComponent< + RestProps = Record +> = (params: ImageComponentParams) => React.JSX.Element | null; + +export type ImageComponentParams< + RestProps = Record +> = { + alt?: string; + image: ImageContent; + imageStyle?: ImageStyle | null; + sizes?: string; + src: string; + srcSet?: string; + style?: React.CSSProperties; +} & RestProps; + +export type ImageContent = Partial & { + imageUrl?: string; +} + +export interface ImageData { + ref: string; + image: ImageContent; + style?: ImageStyle | null; +} + +export interface ImageStyle { + name: string; + aspectRatio: string; + filter: string; +} + +export interface LinkData { + ref: string; + content?: Partial | null; + media?: LinkDataMedia | null; + uri: string; +} + +export interface LinkDataMedia { + content: Partial & { + mediaUrl?: string; + } + intent: 'inline' | 'download'; +} + +export type MacroConfig = Record; + +export interface MacroData { + ref: string; + name: string; + descriptor: MacroDescriptor; + config: Record; +} + +export type MacroDescriptor = `${string}:${string}`; + +export type Replacer = (params: { + el: Element; + data: RichTextData; + mode?: LiteralUnion; +}) => ReplacerResult; + +export type ReplacerResult = JSX.Element | object | void | undefined | null | false; + +export interface RichTextData { + processedHtml: string; + links?: LinkData[]; + macros?: MacroData[]; + images?: ImageData[]; +} + +export type LinkComponent< + RestProps = Record +> = (params: LinkComponentParams) => React.JSX.Element | null; + +export type LinkComponentParams< + RestProps = Record +> = { + children: ReactNode; + content?: Partial | null; + href: string; + media?: LinkDataMedia | null; + target?: string; + title?: string; + uri: string; +} & RestProps; + +export type MacroComponent< + RestProps = Record +> = (params: MacroComponentParams) => React.JSX.Element | null; + +export type MacroComponentParams< + RestProps = Record +> = { + children: string | React.JSX.Element | React.JSX.Element[]; + componentRegistry?: ComponentRegistry; + config: Record; + descriptor: MacroDescriptor; + mode?: LiteralUnion; +} & RestProps; + +export interface ReplaceMacroParams> { + componentRegistry?: ComponentRegistry; + createReplacer: CreateReplacer; + data: RichTextData; + el: Element; + Image: ImageComponent; + Link: LinkComponent; + Macro: MacroComponent; + mode?: LiteralUnion; + replacer?: Replacer; +} + +export type RichtextContent< + Extensions extends Record = Record +> = { + // Direct Properties + _id: string; + _name: string; + _path: string; + _references: RichtextContent[]; + _score?: number; + // attachments: Attachment[]; + children: RichtextContent[]; + // childrenConnection: ContentConnection; + components: RichtextContent[]; + // contentType: ContentType; + createdTime: string; + // creator: PrincipalKey; + // dataAsJson: GraphQLJson; + displayName: string; + // hasChildren: GraphQLBoolean; + language: string; + modifiedTime?: string; + // modifier: PrincipalKey; + // owner: PrincipalKey; + // pageAsJson: GraphQLJson; + pageTemplate: RichtextContent; + pageUrl: string; + parent: RichtextContent; + // permissions: Permissions; + // publish: PublishInfo; + // site: portal_Site; + type: string; + valid: boolean; + // x: ExtraData; + // xAsJson: GraphQLJson; + + // ... on media_Image + imageUrl?: string; + mediaUrl?: string; + [key: string]: any; +} & Extensions + +export type RichTextParams< + RestProps = Record +> = { + className?: string; + componentRegistry?: ComponentRegistry; + data: RichTextData; + Image?: ImageComponent; + Macro?: MacroComponent; + mode?: LiteralUnion; + Link?: LinkComponent; + replacer?: Replacer; + tag?: string; +} & RestProps; diff --git a/src/types/TextBaseProps.ts b/src/types/TextBaseProps.ts new file mode 100644 index 0000000..02632bd --- /dev/null +++ b/src/types/TextBaseProps.ts @@ -0,0 +1,13 @@ +// There is a difference between the core enonic types and what Guillotine returns: +import type { LiteralUnion } from '@enonic-types/core'; +import type { ClassValue } from 'clsx'; +import type { RichTextData } from './RichText'; + +export interface TextBaseProps extends Omit< + React.HTMLAttributes,'className' | 'children' +> { + as?: LiteralUnion; + className?: ClassValue; + data: RichTextData; + 'data-portal-component-type'?: 'text'; +} diff --git a/src/types/index.ts b/src/types/index.ts index 88c5f97..1203a9d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,165 +1,82 @@ // There is a difference between the core enonic types and what Guillotine returns: -// import type {Content} from '@enonic-types/core'; +import type { + Content, + Layout, + LayoutComponent, + LiteralUnion, + Part, + PartComponent, + Page, + PageComponent, + RequestMode, +} from '@enonic-types/core'; +import type { ComponentRegistry } from './ComponentRegistry'; +import type { TextBaseProps } from './TextBaseProps'; // The Guillotine types are similar, but uses complex types: // import type {Content} from '@enonic-types/guillotine/advanced'; -import type {Element} from 'html-react-parser'; -import type {ReactNode, JSX} from 'react'; - - -export declare type Content< - Extensions extends Record = Record -> = { - // Direct Properties - _id: string - _name: string - _path: string - _references: Content[] - _score?: number - // attachments: Attachment[] - children: Content[] - // childrenConnection: ContentConnection - components: Content[] - // contentType: ContentType - createdTime: string - // creator: PrincipalKey - // dataAsJson: GraphQLJson - displayName: string - // hasChildren: GraphQLBoolean - language: string - modifiedTime?: string - // modifier: PrincipalKey - // owner: PrincipalKey - // pageAsJson: GraphQLJson - pageTemplate: Content - pageUrl: string - parent: Content - // permissions: Permissions - // publish: PublishInfo - // site: portal_Site - type: string - valid: boolean - // x: ExtraData - // xAsJson: GraphQLJson - - // ... on media_Image - imageUrl?: string - mediaUrl?: string - [key: string]: any -} & Extensions - -export declare type ContentUri = `content://${string}`; - -export declare type ImageContent = Partial & { - imageUrl?: string -} - -export declare type ImageComponent< - RestProps = Record -> = (params: ImageComponentParams) => React.JSX.Element | null; - -export declare type ImageComponentParams< - RestProps = Record -> = { - alt?: string - image: ImageContent - imageStyle?: ImageStyle | null - sizes?: string - src: string - srcSet?: string - style?: React.CSSProperties -} & RestProps; - -export declare interface ImageData { - ref: string - image: ImageContent, - style?: ImageStyle | null -} - -export declare interface ImageStyle { - name: string - aspectRatio: string - filter: string -} - -export declare type LinkComponent< - RestProps = Record -> = (params: LinkComponentParams) => React.JSX.Element | null; - -export declare type LinkComponentParams< - RestProps = Record -> = { - children: ReactNode - content?: Partial | null - href: string - media?: LinkDataMedia | null - target?: string - title?: string - uri: string -} & RestProps; - -export declare interface LinkData { - ref: string - content?: Partial | null - media?: LinkDataMedia | null - uri: string +export type { + ComponentDefinition, + ComponentDefinitionParams, + ComponentDictionary, + ComponentRegistry, +} from './ComponentRegistry'; +export type { + RenderableComponent, + RenderableContentType, + RenderableLayoutComponent, + RenderablePageComponent, + RenderablePartComponent, + RenderableTextComponent, + RenderableWarning, + XpRunMode, +} from './Renderable'; +export type { + CreateReplacerParams, + ImageComponent, + ImageComponentParams, + ImageContent, + ImageData, + ImageStyle, + LinkComponent, + LinkComponentParams, + LinkData, + LinkDataMedia, + MacroComponent, + MacroComponentParams, + MacroConfig, + MacroData, + MacroDescriptor, + ReplaceMacroParams, + Replacer, + ReplacerResult, + RichtextContent, + RichTextData, + RichTextParams, +} from './RichText'; + +export type ContentUri = `content://${string}`; + + +export type FragmentContent< + Component extends LayoutComponent | PartComponent = Layout | Part +> = Content; + +export type MediaUri = `media://${string}`; + +export type PageContent< + Data = Record, + Type extends string = string, + Component extends PageComponent = Page +> = Content< + Data, + Type, + // @ts-expect-error Does not satisfy the type constraint + Component +> + +export interface TextProps extends TextBaseProps { + componentRegistry: ComponentRegistry; + mode: LiteralUnion } - -export declare interface LinkDataMedia { - content: Partial & { - mediaUrl?: string - } - intent: 'inline' | 'download' -} - -export declare type MacroConfig = Record; - -export declare type MacroComponent< - RestProps = Record -> = (params: MacroComponentParams) => React.JSX.Element | null - -export declare type MacroComponentParams< - RestProps = Record -> = { - config: Record - descriptor: MacroDescriptor - children: string | React.JSX.Element | React.JSX.Element[] -} & RestProps; - -export declare interface MacroData { - ref: string - name: string - descriptor: MacroDescriptor - config: Record -} - -export declare type MacroDescriptor = `${string}:${string}`; - -export declare type MediaUri = `media://${string}`; - -export declare type Replacer = ( - element: Element, - data: RichTextData, -) => ReplacerResult; - -export declare type ReplacerResult = JSX.Element | object | void | undefined | null | false; - -export declare interface RichTextData { - processedHtml: string - links?: LinkData[] - macros?: MacroData[] - images?: ImageData[] -} - -export declare type RichTextParams< - RestProps = Record -> = { - className?: string - data: RichTextData - Image?: ImageComponent - Macro?: MacroComponent - Link?: LinkComponent - replacer?: Replacer - tag?: string -} & RestProps; diff --git a/test/ComponentRegistry/BasePart.test.tsx b/test/ComponentRegistry/BasePart.test.tsx new file mode 100644 index 0000000..8bf836f --- /dev/null +++ b/test/ComponentRegistry/BasePart.test.tsx @@ -0,0 +1,111 @@ +import type { + Request +} from '@enonic-types/core'; +// import type {InfoPanelProps} from '../processComponents/InfoPanel'; +import type {RenderablePartComponent} from '../../src/types'; + +import { + // beforeAll, + // afterAll, + describe, + expect, + test as it +} from '@jest/globals'; +import {render} from '@testing-library/react' +import toDiffableHtml from 'diffable-html'; +import {print} from 'q-i'; +import * as React from 'react'; + +// SRC imports +import {ComponentRegistry} from '../../src/ComponentRegistry/ComponentRegistry'; +import {Part} from '../../src/ComponentRegistry/Part'; +// import {ComponentProcessor} from '../../src/processComponents'; + +// TEST imports +import {InfoPanel} from './InfoPanel'; +import { + EXAMPLE_PART_DESCRIPTOR, + PAGE_CONTENT, + PART_COMPONENT, + PROCESSED_HTML +} from './data' +import {PART_SCHEMA} from './schema' +import {ExamplePart} from './ExamplePart'; + +// const componentProcessor = new ComponentProcessor({ +// getComponentSchema: () => { +// return PART_SCHEMA; +// }, +// // @ts-expect-error +// getContentByKey: ({key}) => { +// // console.debug("getContentByKey:", key); +// return {}; +// }, +// listSchemas: ({ +// application, +// type +// }) => { +// // console.debug("listSchemas:", application, type); +// return []; +// }, +// processHtml: ({ value }) => { +// // console.info("processHtml:", value); +// return PROCESSED_HTML; +// }, +// }); + +// componentProcessor.addPart(EXAMPLE_PART_DESCRIPTOR, { +// toProps: ({ +// component, +// content, +// processedConfig, +// request, +// }) => { +// // console.debug("addPart:", { component, content, processedConfig, request }); +// return { +// data: processedConfig.anHtmlArea +// }; +// }, +// }); + +const componentRegistry = new ComponentRegistry; +// const macroName = 'com.enonic.app.react4xp:info'; // NOPE, just 'info' +const macroName = 'info'; +componentRegistry.addMacro(macroName, { + View: InfoPanel +}); +componentRegistry.addPart(EXAMPLE_PART_DESCRIPTOR, { + View: ExamplePart +}); + +describe('ComponentRegistry', () => { + it.skip('should be able to render a part component', () => { + // const processedComponent = componentProcessor.process({ + // component: PART_COMPONENT, + // content: PAGE_CONTENT, + // request: {} as Request, + // }) as RenderablePartComponent; + // print(processedComponent, { maxItems: Infinity }); +// const element = render().container; +// expect(toDiffableHtml(element.outerHTML)).toEqual(toDiffableHtml(` +//
+//
+//
+//
+// +// +// +// Header +// +// Text +//
+//
+//
+//
+// `)); + // expect(componentRegistry.hasMacro(macroName)).toBe(false); + }); +}); diff --git a/test/ComponentRegistry/ComponentRegistry.test.tsx b/test/ComponentRegistry/ComponentRegistry.test.tsx new file mode 100644 index 0000000..6d98772 --- /dev/null +++ b/test/ComponentRegistry/ComponentRegistry.test.tsx @@ -0,0 +1,41 @@ +import type {InfoPanelProps} from './InfoPanel'; + +import { + // beforeAll, + // afterAll, + describe, + expect, + test as it +} from '@jest/globals'; +import {ComponentRegistry} from '../../src/ComponentRegistry/ComponentRegistry'; +import {ExamplePart} from './ExamplePart'; +import {InfoPanel} from './InfoPanel'; + +const componentRegistry = new ComponentRegistry; + +describe('ComponentRegistry', () => { + it('should be able to register macro components', () => { + const macroName = 'com.enonic.app.react4xp:info'; + expect(componentRegistry.hasMacro(macroName)).toBe(false); + expect(componentRegistry.getMacro(macroName)).toBe(undefined); + componentRegistry.addMacro(macroName, { + View: InfoPanel + }); + expect(componentRegistry.hasMacro(macroName)).toBe(true); + expect(componentRegistry.getMacro(macroName)).toEqual({ + View: InfoPanel + }); + }); + it('should be able to register part components', () => { + const partName = 'com.enonic.app.react4xp:example'; + expect(componentRegistry.hasPart(partName)).toBe(false); + expect(componentRegistry.getPart(partName)).toBe(undefined); + componentRegistry.addPart(partName, { + View: ExamplePart + }); + expect(componentRegistry.hasPart(partName)).toBe(true); + expect(componentRegistry.getPart(partName)).toEqual({ + View: ExamplePart + }); + }); +}); diff --git a/test/ComponentRegistry/DefaultPage.tsx b/test/ComponentRegistry/DefaultPage.tsx new file mode 100644 index 0000000..e7c7abf --- /dev/null +++ b/test/ComponentRegistry/DefaultPage.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import {XpRegions} from '../../src/ComponentRegistry/XpRegions'; + +export function DefaultPage(props) { + // console.debug('DefaultPage props', props); + const { + componentRegistry, + regions, + } = props; + return ( +
+ +
+ ); +} diff --git a/test/ComponentRegistry/ExamplePart.tsx b/test/ComponentRegistry/ExamplePart.tsx new file mode 100644 index 0000000..db904a7 --- /dev/null +++ b/test/ComponentRegistry/ExamplePart.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { RichText } from '../../src/RichText/RichText'; + +export function ExamplePart(props) { + // console.debug('ExamplePart props', props); + + const { + componentRegistry, + data, + } = props; + // console.debug('ExamplePart data', data); + + return ( +
+ +
+ ); +} diff --git a/test/ComponentRegistry/InfoPanel.tsx b/test/ComponentRegistry/InfoPanel.tsx new file mode 100644 index 0000000..21ae390 --- /dev/null +++ b/test/ComponentRegistry/InfoPanel.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; + +export interface InfoPanelProps { + body: string + header: string +} + +export function InfoPanel({ + body, + header, +}: InfoPanelProps) { + return
{header}{body}
; +} diff --git a/test/ComponentRegistry/TwoColumnLayout.tsx b/test/ComponentRegistry/TwoColumnLayout.tsx new file mode 100644 index 0000000..231122b --- /dev/null +++ b/test/ComponentRegistry/TwoColumnLayout.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import {XpRegions} from '../../src/ComponentRegistry/XpRegions'; + +export function TwoColumnLayout(props) { + // console.debug('TwoColumnLayout props', props); + const { + componentRegistry, + regions, + } = props; + return ( +
+ +
+ ); +} diff --git a/test/ComponentRegistry/constants.ts b/test/ComponentRegistry/constants.ts new file mode 100644 index 0000000..dbe20a4 --- /dev/null +++ b/test/ComponentRegistry/constants.ts @@ -0,0 +1,3 @@ +export const HTML_AREA_KEY = 'anHtmlArea'; +export const ITEM_SET_KEY = 'anItemSet'; +export const OPTION_SET_KEY = 'anOptionSet'; diff --git a/test/ComponentRegistry/data.ts b/test/ComponentRegistry/data.ts new file mode 100644 index 0000000..a075b38 --- /dev/null +++ b/test/ComponentRegistry/data.ts @@ -0,0 +1,256 @@ + +import type { + Content, + LayoutComponent, + PageComponent, + PartComponent, + FragmentComponent, + TextComponent, +} from '@enonic-types/core'; +import type {PageContent} from '../../src/types'; + +import { + HTML_AREA_KEY, + ITEM_SET_KEY, + OPTION_SET_KEY, +} from './constants'; + +export const UNPROCESSED_HTML = '

[info header="Header"]Text[/info]

'; +export const PROCESSED_HTML = `

` + +export const CONFIG = { + [HTML_AREA_KEY]: UNPROCESSED_HTML, + [ITEM_SET_KEY]: { + [HTML_AREA_KEY]: UNPROCESSED_HTML, + }, + [OPTION_SET_KEY]: [ + { + hr: {}, + _selected: "hr", + }, + { + text: { + [HTML_AREA_KEY]: UNPROCESSED_HTML, + }, + _selected: "text", + }, + ], +}; + +export const TEXT_COMPONENT: TextComponent = { + path: "/main/0", + type: "text", + text: UNPROCESSED_HTML, +}; + +export const EXAMPLE_PART_DESCRIPTOR = "com.enonic.app.react4xp:example"; + +export const PART_COMPONENT: PartComponent = { + path: "/main/0/left/0", + type: "part", + descriptor: EXAMPLE_PART_DESCRIPTOR, + config: CONFIG, +}; + +export const LAYOUT_FRAGMENT_CONTENT_ID = '8c926279-39bd-4bff-a502-ffe921b95ada'; +export const PART_FRAGMENT_CONTENT_ID = '6a71fd9e-f9fc-4395-8954-1d67a5e35bf3'; +export const TEXT_FRAGMENT_CONTENT_ID = 'a641b21d-1af3-4559-a936-9e1ab71a19c4'; + +export const PART_FRAGMENT_COMPONENT: FragmentComponent = { + path: "/main/0", + type: "fragment", + fragment: PART_FRAGMENT_CONTENT_ID, +}; + +export const TEXT_FRAGMENT_COMPONENT: FragmentComponent = { + path: "/main/1", + type: "fragment", + fragment: TEXT_FRAGMENT_CONTENT_ID, +}; + +export const LAYOUT_FRAGMENT_COMPONENT: FragmentComponent = { + path: "/main/0", + type: "fragment", + fragment: LAYOUT_FRAGMENT_CONTENT_ID, +}; + +export const TWO_COLUMNS_LAYOUT_DESCRIPTOR = "com.enonic.app.react4xp:twoColumns"; + +export const LAYOUT_FRAGMENT_CONTENT = { + _id: LAYOUT_FRAGMENT_CONTENT_ID, + _name: "fragment-two-columns", + _path: "/mysite/page-with-layout-fragment-with-text-and-part-fragment/fragment-two-columns", + creator: "user:system:su", + modifier: "user:system:su", + createdTime: "2024-11-05T09:38:31.175061Z", + modifiedTime: "2024-11-05T09:38:31.280283Z", + owner: "user:system:su", + type: "portal:fragment", + displayName: "Two columns", + hasChildren: false, + valid: true, + childOrder: "modifiedtime DESC", + data: {}, + x: {}, + fragment: { + type: "layout", + descriptor: TWO_COLUMNS_LAYOUT_DESCRIPTOR, + config: { + myhtmlarea: '

[info header="Header"]Text[/info]

\n', + }, + regions: { + left: { + components: [ + { + path: "/left/0", + type: "fragment", + fragment: TEXT_FRAGMENT_CONTENT_ID, + }, + { + path: "/left/1", + type: "fragment", + fragment: PART_FRAGMENT_CONTENT_ID, + }, + ], + name: "left", + }, + }, + }, + attachments: {}, + publish: {}, +}; + +export const TEXT_FRAGMENT_CONTENT = { + _id: TEXT_FRAGMENT_CONTENT_ID, + _name: "fragment-info-header-header-text-info", + _path: "/mysite/fragment-info-header-header-text-info", + creator: "user:system:su", + modifier: "user:system:su", + createdTime: "2024-11-05T09:13:48.488533Z", + modifiedTime: "2024-11-05T09:13:48.568588Z", + owner: "user:system:su", + type: "portal:fragment", + displayName: '[info header="Header"]Text[/info]', + hasChildren: false, + valid: true, + childOrder: "modifiedtime DESC", + data: {}, + x: {}, + fragment: { + type: 'text', + text: UNPROCESSED_HTML, + }, + attachments: {}, + publish: {}, +}; + +export const PART_FRAGMENT_CONTENT = { + _id: PART_FRAGMENT_CONTENT_ID, + _name: "fragment-example", + _path: "/mysite/with-fragment/fragment-example", + creator: "user:system:su", + modifier: "user:system:su", + createdTime: "2024-10-30T08:19:49.053500Z", + modifiedTime: "2024-10-30T08:19:49.122382Z", + owner: "user:system:su", + type: "portal:fragment", + displayName: "example", + hasChildren: false, + valid: true, + childOrder: "modifiedtime DESC", + data: {}, + x: {}, + fragment: { + type: "part", + descriptor: EXAMPLE_PART_DESCRIPTOR, + config: CONFIG, + }, + attachments: {}, + publish: {}, +}; + +export const LAYOUT_COMPONENT: LayoutComponent = { + path: '/main/0', + type: 'layout', + descriptor: TWO_COLUMNS_LAYOUT_DESCRIPTOR, + config: CONFIG, + regions: { + left: { + components: [ + // TEXT_COMPONENT, + // TEXT_FRAGMENT_COMPONENT, + {...PART_COMPONENT, path: '/main/0/left/0'}, + // PART_FRAGMENT_COMPONENT, + ], + name: "left", + }, + right: { + components: [ + // { + // ...PART_FRAGMENT_COMPONENT, + // path: "/main/0/right/0", + // } + ], + name: "right", + }, + }, +}; + +export const DEFAULT_PAGE_DESCRIPTOR = "com.enonic.app.react4xp:default"; + +export const PAGE_COMPONENT: PageComponent = { + type: "page", + path: "/", + descriptor: DEFAULT_PAGE_DESCRIPTOR, + config: CONFIG, + regions: { + main: { + components: [ + // TEXT_COMPONENT, + // TEXT_FRAGMENT_COMPONENT, + // LAYOUT_COMPONENT, + {...PART_COMPONENT, path: "/main/0"}, + // LAYOUT_FRAGMENT_COMPONENT, + // { + // ...PART_FRAGMENT_COMPONENT, + // path: "/main/1", + // } + ], + name: "main", + }, + }, +}; + +export const PAGE_CONTENT: PageContent = { + _id: "3e36f69a-fa2f-4943-a812-5a2d06f22e56", + _name: "mysite", + _path: "/mysite", + creator: "user:system:su", + modifier: "user:system:su", + createdTime: "2024-10-30T08:14:10.575402Z", + modifiedTime: "2024-11-05T09:36:11.782402Z", + owner: "user:system:su", + type: "portal:site", + displayName: "mysite", + hasChildren: true, + valid: true, + childOrder: "modifiedtime DESC", + data: { + siteConfig: [ + { + applicationKey: "com.enonic.app.react4xp", + config: {}, + }, + { + applicationKey: "com.enonic.app.panelmacros", + config: { + custom: false, + }, + }, + ], + }, + x: {}, + page: PAGE_COMPONENT, + attachments: {}, + publish: {}, +}; diff --git a/test/ComponentRegistry/schema.ts b/test/ComponentRegistry/schema.ts new file mode 100644 index 0000000..28a832a --- /dev/null +++ b/test/ComponentRegistry/schema.ts @@ -0,0 +1,247 @@ +import type { + // ContentSchemaType, + // FormItem, + FormItemInput, + FormItemOptionSet, + FormItemSet, + // ListDynamicSchemasParams, + MixinSchema, + // XDataSchema, +} from '@enonic-types/lib-schema'; +import type { + GetComponentReturnType, + NestedPartial +} from '../../src/processComponents'; + +import { + HTML_AREA_KEY, + ITEM_SET_KEY, + OPTION_SET_KEY, +} from './constants'; + + +export const FORM_ITEM_INPUT: Partial = { + formItemType: "Input", + name: HTML_AREA_KEY, + label: "My HtmlArea", + maximize: true, + inputType: "HtmlArea", + occurrences: { + maximum: 1, + minimum: 0, + }, + config: {}, +}; + +export const FORM_ITEM_SET: Partial = { + formItemType: "ItemSet", + name: ITEM_SET_KEY, + label: "Contact Info", + occurrences: { + maximum: 0, + minimum: 0, + }, + items: [ + FORM_ITEM_INPUT as FormItemInput, + ], +}; + +export const FORM_ITEM_OPTION_SET: FormItemOptionSet = { + formItemType: "OptionSet", + name: OPTION_SET_KEY, + label: "Content blocks", + expanded: false, + helpText: "Create content with optional blocks", + occurrences: { + maximum: 0, + minimum: 0, + }, + selection: { + maximum: 1, + minimum: 1, + }, + options: [ + { + name: "hr", + label: "Horisontal line", + helpText: "Adds a separator between blocks", + default: false, + items: [], + }, + { + name: "text", + label: "Text", + default: false, + items: [ + // @ts-expect-error + FORM_ITEM_INPUT + ], + }, + ], +}; + +const FORM = [ + FORM_ITEM_INPUT, + FORM_ITEM_SET, + FORM_ITEM_OPTION_SET, +]; + +export const MIXIN_SCHEMAS: NestedPartial = [ + { + name: "com.enonic.app.react4xp:mymixin", + displayName: "My mixin", + displayNameI18nKey: "", + modifiedTime: "2024-11-05T07:23:42Z", + resource:` + My mixin +
+ + + + + + + + + + + + + + + + Create content with optional blocks + + + + + +
+
+`, + type: "MIXIN", + form: FORM, + }, +]; + +export const PART_SCHEMA: GetComponentReturnType = { + key: "com.enonic.app.react4xp:example", + displayName: "Example Part", + displayNameI18nKey: "", + componentPath: "com.enonic.app.react4xp:/site/parts/example", + modifiedTime: "2024-11-04T07:55:16Z", + resource: ` + Example Part +
+ + + + +
+
+`, + type: "PART", + // form: FORM, + form: [ + // { + // formItemType: "InlineMixin", + // name: "com.enonic.app.react4xp:mymixin", + // }, + { + formItemType: "Layout", + name: "fieldSet30", + label: "FieldSet", + items: FORM, + }, + ], + config: {}, +}; + +export const LAYOUT_SCHEMA: GetComponentReturnType = { + key: "com.enonic.app.react4xp:twoColumns", + displayName: "Two columns", + displayNameI18nKey: "", + description: "Two columns react-rendered layout controller", + descriptionI18nKey: "", + componentPath: "com.enonic.app.react4xp:/site/layouts/twoColumns", + modifiedTime: "2024-11-04T10:09:04Z", + resource: + ` + Two columns + Two columns react-rendered layout controller +
+ + + + + + + + + + + + +
+ + + + +
+ `, + type: "LAYOUT", + form: FORM, + config: {}, + regions: ["left", "right"], +}; + +export const PAGE_SCHEMA: GetComponentReturnType = { + key: "com.enonic.app.react4xp:default", + displayName: "Default page", + displayNameI18nKey: "", + description: "Default react-rendered page controller", + descriptionI18nKey: "", + componentPath: "com.enonic.app.react4xp:/site/pages/default", + modifiedTime: "2024-11-04T10:38:34Z", + resource: ` + Default page + Default react-rendered page controller +
+ + + + + + + + + + + + +
+ + + +
+`, + type: "PAGE", + form: FORM, + config: {}, + regions: ["main"], +}; diff --git a/test/RichText.images.test.tsx b/test/RichText.images.test.tsx index 24e4c36..0eef07d 100644 --- a/test/RichText.images.test.tsx +++ b/test/RichText.images.test.tsx @@ -18,6 +18,10 @@ import React from 'react'; import {RichText} from '../src'; import {Image} from '../src/RichText/Image'; // import {print} from 'q-i'; +import { + ERROR_STYLE, + WARNING_STYLE +} from './testdata'; const IMG_REF = '59b78b11-3abf-4b7e-b16e-a5b1e90efcb0'; @@ -105,9 +109,10 @@ describe('RichText', () => { className='myclass' data={data} Image={ImageThatThrows} + mode='inline' />).baseElement; // print(html.outerHTML, { maxItems: Infinity }); - expect(html.outerHTML).toBe(`
Failed to render image!
+ expect(html.outerHTML).toBe(`
Failed to render image!
Caption
`); }); @@ -228,7 +233,7 @@ describe('RichText', () => { class="captioned editor-align-right editor-width-custom" style="float: right; width: 50%;" > -
+
Can't replace image, when there are no images in the data object!
@@ -269,7 +274,7 @@ describe('RichText', () => { class="captioned editor-align-right editor-width-custom" style="float: right; width: 50%;" > -
+
Image element has no data-image-ref attibute!
@@ -309,7 +314,7 @@ describe('RichText', () => { class="captioned editor-align-right editor-width-custom" style="float: right; width: 50%;" > -
+
Unable to find image with ref ${IMG_REF} in images object!
diff --git a/test/RichText.links.test.tsx b/test/RichText.links.test.tsx index 9287d12..ac123c9 100644 --- a/test/RichText.links.test.tsx +++ b/test/RichText.links.test.tsx @@ -17,7 +17,10 @@ import React from 'react'; import {RichText} from '../src'; import {Link} from '../src/RichText/Link'; // import {print} from 'q-i'; - +import { + ERROR_STYLE, + WARNING_STYLE +} from './testdata'; const IMG_ID = 'e9b1f92b-fa46-4e58-b41f-87dc9f1999e8' const IMG_VERSION_KEY = '9abf6cc6c7f565515175b33c08155b3495dcdf47'; @@ -107,10 +110,11 @@ describe('RichText', () => { const html = render().baseElement; // print(html.outerHTML, { maxItems: Infinity }); - expect(html.outerHTML).toBe(`
Failed to build href!
`); + expect(html.outerHTML).toBe(`
Failed to build href!
`); }); it('should handle links', () => { @@ -164,7 +168,7 @@ describe('RichText', () => {
-
+
Link element has no href attribute!
@@ -182,10 +186,11 @@ describe('RichText', () => { const html = render().baseElement; // print(html.outerHTML, { maxItems: Infinity }); - expect(html.outerHTML).toBe(`
Can't replace link, when there are no links in the data object!
`); + expect(html.outerHTML).toBe(`
Can't replace link, when there are no links in the data object!
`); }); it("should show an ErrorComponent when the linkRef can't be found in the links object", () => { @@ -207,9 +212,10 @@ describe('RichText', () => { const html = render().baseElement; // print(html.outerHTML, { maxItems: Infinity }); - expect(html.outerHTML).toBe(`
Unable to find link with ref ${linkRef} in links object!
`); + expect(html.outerHTML).toBe(`
Unable to find link with ref ${linkRef} in links object!
`); }); }); // describe RichText diff --git a/test/RichText.macros.test.tsx b/test/RichText.macros.test.tsx index f18e796..704aa4b 100644 --- a/test/RichText.macros.test.tsx +++ b/test/RichText.macros.test.tsx @@ -7,6 +7,10 @@ import toDiffableHtml from 'diffable-html'; import React from 'react'; import {RichText} from '../src'; import {Macro} from './RichText/Macro'; +import { + ERROR_STYLE, + WARNING_STYLE, +} from './testdata'; // import {print} from 'q-i'; @@ -86,10 +90,11 @@ describe('RichText', () => { const html = render().baseElement; // print(html.outerHTML, { maxItems: Infinity }); - expect(html.outerHTML).toBe(`

Macro not found: com.enonic.app.panelmacros:failure

`); + expect(html.outerHTML).toBe(`

Macro not found: com.enonic.app.panelmacros:failure

`); }); it('should show an ErrorComponent when the macros array is missing or empty', () => { @@ -102,9 +107,10 @@ describe('RichText', () => { const html = render().baseElement; - expect(html.outerHTML).toBe(`

Can't replace macro, when there are no macros in the data object!

`); + expect(html.outerHTML).toBe(`

Can't replace macro, when there are no macros in the data object!

`); }); it('should show an ErrorComponent when the macro element has not data-macro-ref atribute', () => { @@ -115,9 +121,10 @@ describe('RichText', () => { const html = render().baseElement; - expect(html.outerHTML).toBe(`

Macro element has no data-macro-ref attribute!

`); + expect(html.outerHTML).toBe(`

Macro element has no data-macro-ref attribute!

`); }); it("should show an ErrorComponent when the macroRef isn't found in the macros array", () => { @@ -139,10 +146,11 @@ describe('RichText', () => { } const html = render().baseElement; // print(html.outerHTML, { maxItems: Infinity }); - expect(html.outerHTML).toBe(`

Unable to find macro with ref ${SUCCESS_REF} in macros object!

`); + expect(html.outerHTML).toBe(`

Unable to find macro with ref ${SUCCESS_REF} in macros object!

`); }); it("should render a fallback Macro component, when it's not provided", () => { @@ -165,6 +173,7 @@ describe('RichText', () => { const html = render().baseElement; // print(html.outerHTML, { maxItems: Infinity }); expect(toDiffableHtml(html.outerHTML)).toBe(` @@ -172,7 +181,7 @@ describe('RichText', () => {

-

+
No Macro component provided to RichText. Can't render com.enonic.app.panelmacros:success-macro with config { "__nodeId": "d30c4572-0720-44cb-8137-7c830722b056", "header": "Iha", diff --git a/test/RichText.replacer.test.tsx b/test/RichText.replacer.test.tsx index 68568d8..b117e76 100644 --- a/test/RichText.replacer.test.tsx +++ b/test/RichText.replacer.test.tsx @@ -22,9 +22,16 @@ describe('RichText', () => { } const html = render( { + data={data} + mode='edit' + replacer={({ + el, + data, + mode, + }) => { // console.debug('el', el); // console.debug('data', data); + // console.debug('mode', mode); if ( el.name === 'p' && el.children[0].type === ElementType.Text @@ -33,7 +40,6 @@ describe('RichText', () => { return

Replaced text

; } }} - data={data} />).baseElement; // print(html.outerHTML, { maxItems: Infinity }); expect(toDiffableHtml(html.outerHTML)).toBe(` diff --git a/test/testdata.ts b/test/testdata.ts new file mode 100644 index 0000000..91317eb --- /dev/null +++ b/test/testdata.ts @@ -0,0 +1,2 @@ +export const ERROR_STYLE = 'background-color: rgb(255, 255, 255); border-width: 2px; border-style: dashed; border-radius: 4px; box-sizing: border-box; display: block; font-family: Open Sans, Helvetica, sans-serif; font-size: 20px; line-height: 33px; margin: 1px 0px 10px; min-height: 137px; padding: 50px 15px; position: relative; text-align: center; border-color: #e0b4b4; color: rgb(159, 58, 56);' +export const WARNING_STYLE = 'background-color: rgb(255, 255, 255); border-width: 2px; border-style: dashed; border-radius: 4px; box-sizing: border-box; display: block; font-family: Open Sans, Helvetica, sans-serif; font-size: 20px; line-height: 33px; margin: 1px 0px 10px; min-height: 137px; padding: 50px 15px; position: relative; text-align: center; border-color: #c9ba9b; color: rgb(121, 75, 2);'; diff --git a/test/tsconfig.json b/test/tsconfig.json index de151f0..0d6e1c1 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -5,5 +5,15 @@ "ES2015", "DOM" ], + "paths": { + "@enonic-types/core": ["../../xp-comlock-9742/modules/lib/core/index.d.ts"], + "/lib/xp/content": ["../../xp-comlock-9742/modules/lib/lib-content/src/main/resources/lib/xp/content.ts"], + "/lib/xp/portal": ["../../xp-comlock-9742/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts"], + "/lib/xp/schema": ["../node_modules/@enonic-types/lib-schema"], + }, + "types": [ + "bun", + "jest" + ] } } diff --git a/tsconfig.json b/tsconfig.json index aa1722c..a2958a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,33 +1,40 @@ { // https://www.typescriptlang.org/tsconfig - "compilerOptions": { - "allowUmdGlobalAccess": true, - "declaration": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "lib": [ - "DOM", // NOTE: This ChildNode differs from the one in domhandler npm module. - "ES2015" - ], - "module": "preserve", - "moduleResolution": "bundler", + // Specifies an allowlist of files to include in the program. + // An error occurs if any of the files can’t be found. + // This is useful when you only have a small number of files and don’t need + // to use a glob to reference many files. If you need that then use include. + "files": [ + "src/index.ts" + ], - // Even though the setting disables type checking for d.ts files, - // TypeScript still type checks the code you specifically - // refer to in your application's source code. - "skipLibCheck": true, + "include": [ + "src/**/*.tsx", + "src/**/*.ts" + ], - "strictNullChecks": true, - }, + "compilerOptions": { + "allowUmdGlobalAccess": true, + "declaration": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "lib": [ + "DOM", // NOTE: This ChildNode differs from the one in domhandler npm module. + "ES2015" + ], + "module": "preserve", + "moduleResolution": "bundler", - // Specifies an allowlist of files to include in the program. - // An error occurs if any of the files can’t be found. - // This is useful when you only have a small number of files and don’t need - // to use a glob to reference many files. If you need that then use include. - "files": [ - "src/index.ts" - ], + "paths": { + // "@enonic-types/core": ["../xp/modules/lib/core/index.d.ts"], + // "/lib/xp/content": ["../xp/modules/lib/lib-content/src/main/resources/lib/xp/content.ts"], + // "/lib/xp/portal": ["../xp/modules/lib/lib-portal/src/main/resources/lib/xp/portal.ts"], + }, - "include": [ - "src/**/*.ts" - ], + // Even though the setting disables type checking for d.ts files, + // TypeScript still type checks the code you specifically + // refer to in your application's source code. + "skipLibCheck": true, + + "strictNullChecks": true + } } diff --git a/tsup.config.ts b/tsup.config.ts index f5df8be..15f38f9 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import type { Options } from 'tsup'; -import { globSync } from 'glob'; +import GlobalsPlugin from 'esbuild-plugin-globals'; import { defineConfig} from 'tsup'; @@ -10,30 +10,74 @@ interface MyOptions extends Options { } -const GLOB_EXTENSIONS = '{ts,tsx}'; -const TS_FILES = globSync(`./src/**/*.${GLOB_EXTENSIONS}`, { - absolute: false, - ignore: [] -}).map(dir => dir.replace(/\\/g,'/')); +const ENTRY = [ + 'src/index.ts', + 'src/nashorn.ts' +]; +const external = []; + +const noExternal = [ + '@enonic/js-utils', // We only use toStr which is so small, might as well just bundle it. + + // Maybe these could remain dependencies in package.json, + // but currently that seems to cause runtime errors with starter-react4xp. + 'clsx', + 'domelementtype', + + // The entities node module contains Uint16Array which fails in Nashorn. + // html-react-parser has multiple layers of dependencies that ends up dragging in entities. + // html-react-parser -> html-dom-parser -> htmlparser2 -> domutils -> dom-serializer -> entities + // To avoid the problem we bundle the server variant of html-dom-parser which does not depend on entities. + // As such is should be kept a devDependency in the package.json and be listed as an external here: + 'html-react-parser', + + 'react', // For GlobalsPlugin to work react MUST be listed here (if react under dependencies or peerDependencies) +]; + +const esbuildPlugins = [ + GlobalsPlugin({ + react: 'React', + }), +]; export default defineConfig((options: MyOptions) => { if (Array.isArray(options.format) && options.format[0] === 'cjs') { return { + bundle: true, d: 'dist', dts: false, - entry: TS_FILES, + entry: ENTRY, + esbuildOptions(options) { + options.alias = { + "html-dom-parser": "./node_modules/html-dom-parser/lib/server/html-to-dom.js", + } + }, + esbuildPlugins, + external, minify: false, + noExternal, platform: 'neutral', target: 'es5', sourcemap: false, + splitting: true, + tsconfig: './tsconfig.json', }; } else if (Array.isArray(options.format) && options.format[0] === 'esm') { return { + bundle: true, d: 'dist', dts: false, - entry: TS_FILES, + entry: ENTRY, + esbuildOptions(options) { + options.alias = { + "html-dom-parser": "./node_modules/html-dom-parser/esm/server/html-to-dom.mjs", + } + }, + esbuildPlugins, + external, minify: false, + noExternal, outExtension() { return { js: '.mjs' @@ -41,8 +85,9 @@ export default defineConfig((options: MyOptions) => { }, platform: 'neutral', target: 'es2015', - splitting: false, // avoid chunk files + splitting: true, sourcemap: false, + tsconfig: './tsconfig.json', }; } throw new Error(`Unsupported format:${options.format}!`)