From 07653734618397a66f9284a7892f0f687a57832f Mon Sep 17 00:00:00 2001 From: shobhit-cstk Date: Tue, 21 Apr 2026 13:00:58 +0530 Subject: [PATCH 01/63] feat(contentful): taxonomy migration, locale resolution, tests and config - Extract and map Contentful taxonomies from export; upload-api and mapper integration - Contentful service: field/widget helpers, taxonomy metadata locale resolution (mapper + sys.locale) - API: Vitest thresholds; migration and user unit tests (SSO, createTaxonomy mocks) - app.json placeholder updates and related fixes --- api/package-lock.json | 1059 +++++++++-------- api/package.json | 8 +- api/src/services/contentMapper.service.ts | 48 + api/src/services/contentful.service.ts | 212 +++- .../services/contentful/taxonomy.service.ts | 237 ++++ api/src/services/migration.service.ts | 10 + api/src/services/wordpress.service.ts | 6 +- api/src/utils/content-type-creator.utils.ts | 169 ++- .../unit/services/migration.service.test.ts | 1 + api/tests/unit/services/user.service.test.ts | 129 +- api/vitest.config.ts | 6 +- app.json | 28 +- ui/package-lock.json | 138 +-- ui/package.json | 6 +- ui/src/components/AdvancePropertise/index.tsx | 74 +- .../__tests__/groupSchema.utils.test.ts | 44 + .../ContentMapper/groupSchema.utils.ts | 22 + ui/src/components/ContentMapper/index.scss | 6 + ui/src/components/ContentMapper/index.tsx | 455 +++---- upload-api/migration-aem/package-lock.json | 19 + upload-api/migration-contentful/index.js | 4 +- .../libs/createInitialMapper.js | 62 +- .../libs/extractTaxonomy.js | 59 + .../migration-wordpress/libs/extractItems.ts | 22 +- .../libs/extractTaxonomy.ts | 2 + .../migration-wordpress/libs/schemaMapper.ts | 39 +- upload-api/src/config/index.ts | 2 +- upload-api/src/services/contentful/index.ts | 33 +- .../migration-wordpress/schemaMapper.test.ts | 6 +- 29 files changed, 1961 insertions(+), 945 deletions(-) create mode 100644 api/src/services/contentful/taxonomy.service.ts create mode 100644 upload-api/migration-contentful/libs/extractTaxonomy.js diff --git a/api/package-lock.json b/api/package-lock.json index 943e44a8c..e219bca0d 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -336,29 +336,29 @@ } }, "node_modules/@contentstack/cli": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli/-/cli-1.60.0.tgz", - "integrity": "sha512-Zr5Dl93NO4CpAtpX+pudMETg4r7TePUCi2wZWaukgaxQ8WfigjewM04yAy07nV/RFKKcstI3EBOpWkXee/X1jA==", + "version": "1.60.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli/-/cli-1.60.1.tgz", + "integrity": "sha512-tH8WJ1khzaHjLuvtGAAeAoqPOzT5EkTM20dLx3sOKvm/4Izj+gKffcbsEwtlVtI45UO5dJMiGDHLYnxGij9Rgw==", "license": "MIT", "dependencies": { - "@contentstack/cli-audit": "~1.19.0", + "@contentstack/cli-audit": "~1.19.1", "@contentstack/cli-auth": "~1.8.0", "@contentstack/cli-cm-bootstrap": "~1.19.0", - "@contentstack/cli-cm-branches": "~1.7.0", - "@contentstack/cli-cm-bulk-publish": "~1.11.0", - "@contentstack/cli-cm-clone": "~1.21.0", + "@contentstack/cli-cm-branches": "~1.7.1", + "@contentstack/cli-cm-bulk-publish": "~1.11.1", + "@contentstack/cli-cm-clone": "~1.21.1", "@contentstack/cli-cm-export": "~1.24.0", "@contentstack/cli-cm-export-to-csv": "~1.12.0", "@contentstack/cli-cm-import": "~1.32.0", - "@contentstack/cli-cm-import-setup": "~1.8.0", + "@contentstack/cli-cm-import-setup": "~1.8.1", "@contentstack/cli-cm-migrate-rte": "~1.6.4", "@contentstack/cli-cm-seed": "~1.15.0", "@contentstack/cli-command": "~1.8.0", - "@contentstack/cli-config": "~1.20.0", - "@contentstack/cli-launch": "^1.9.6", + "@contentstack/cli-config": "~1.20.1", + "@contentstack/cli-launch": "^1.9.7", "@contentstack/cli-migration": "~1.12.0", - "@contentstack/cli-utilities": "~1.18.0", - "@contentstack/cli-variants": "~1.4.0", + "@contentstack/cli-utilities": "~1.18.1", + "@contentstack/cli-variants": "~1.4.1", "@contentstack/management": "~1.27.5", "@oclif/core": "^4.8.3", "@oclif/plugin-help": "^6.2.28", @@ -383,9 +383,9 @@ } }, "node_modules/@contentstack/cli-audit": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-audit/-/cli-audit-1.19.0.tgz", - "integrity": "sha512-NwET9cBeCmM5tM3faJ8wG4YMX9JrGYo7lYE7WQhmVUOhC/7bbYKPpWb6Jmy3JMvzP9YSwmSrztEUdwotB4F6WQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-audit/-/cli-audit-1.19.1.tgz", + "integrity": "sha512-k3bu/NLXGu7/ntMtWh/kd4smytQ44Z4wlixCajoPxKR2k1A/4OaZD6n1WQ7TcJ7biosNU9Pl83PO4oKMdxrEfA==", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.8.0", @@ -396,7 +396,7 @@ "chalk": "^4.1.2", "fast-csv": "^4.3.6", "fs-extra": "^11.3.0", - "lodash": "^4.17.23", + "lodash": "4.18.1", "uuid": "^9.0.1", "winston": "^3.17.0" }, @@ -456,9 +456,9 @@ } }, "node_modules/@contentstack/cli-cm-branches": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-branches/-/cli-cm-branches-1.7.0.tgz", - "integrity": "sha512-5uCqqzB1mGlHX8Ac3+3bteUuQ0CNBHmAZ6mRj9D6/DPTW7spbOHkKWksMyKuSrbkOTj57otjZUpcWvZobuW5kg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-branches/-/cli-cm-branches-1.7.1.tgz", + "integrity": "sha512-IUU/Hs7/LXH/vGRkqGf+CQhDSMFLLa0KqYLOi+LneBU/irrQSC6ue+/oaGVJw4i59Wy/rV5U3buCreAKlSzd2Q==", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.8.0", @@ -467,16 +467,16 @@ "@oclif/plugin-help": "^6.2.28", "chalk": "^4.1.2", "just-diff": "^6.0.2", - "lodash": "^4.17.23" + "lodash": "4.18.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@contentstack/cli-cm-bulk-publish": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-bulk-publish/-/cli-cm-bulk-publish-1.11.0.tgz", - "integrity": "sha512-oQd1se/3qa18exmuIFvBL+zH3wuSWHuS4eV1nAyYA6pVl8HU4hqcCj1ygHdMdUx7ZLckESgZjeOfeglbUJg3zQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-bulk-publish/-/cli-cm-bulk-publish-1.11.1.tgz", + "integrity": "sha512-0mjpOfzSMX/vJFXiLhQwgefCuVuiqBH2e/8BR2ks4a6/8ISquIcACkJ7Zosh+8LjiAxSOSJtCtykn6nJIaV2EA==", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.8.0", @@ -487,7 +487,7 @@ "chalk": "^4.1.2", "dotenv": "^16.5.0", "inquirer": "8.2.7", - "lodash": "^4.17.23", + "lodash": "4.18.1", "winston": "^3.17.0" }, "engines": { @@ -495,9 +495,9 @@ } }, "node_modules/@contentstack/cli-cm-clone": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-clone/-/cli-cm-clone-1.21.0.tgz", - "integrity": "sha512-StImHRgX+9iGjdnFOM3AAdU7+ccOrMk3mr1p0oaIlqPLu5QDrMqNZialhuV0BjZ0BXPhLm93bpXuEKg0cCK+5w==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-clone/-/cli-cm-clone-1.21.1.tgz", + "integrity": "sha512-s/UJhEtYqjPKhLbys0eVoDz+yYgESSki5Z+4jQr/PaGcoW3GM4hDro1d+c/rG3/KhKy4VdhnZZcPVDdezRSsvQ==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", @@ -509,7 +509,7 @@ "@oclif/plugin-help": "^6.2.28", "chalk": "^4.1.2", "inquirer": "8.2.7", - "lodash": "^4.17.23", + "lodash": "4.18.1", "merge": "^2.1.1", "ora": "^5.4.1", "prompt": "^1.3.0", @@ -520,9 +520,9 @@ } }, "node_modules/@contentstack/cli-cm-export": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-export/-/cli-cm-export-1.24.0.tgz", - "integrity": "sha512-eRdSt3z08W7MNH1CnXhZSwJ4o/jodYIBGv+f1G3Zs3ZrthNToDQBR7018gmVjjoZYXIRMvaHo8Nw8eZgbYtchA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-export/-/cli-cm-export-1.24.1.tgz", + "integrity": "sha512-zTaun28JcIjT88NCi+p1gZabZVkJO6bQLOBs+QGHPOUxRBn5t++AYOuomm7iA1ZJAzsfMN5FF1lWRuRZ2qPf2A==", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.8.0", @@ -533,7 +533,7 @@ "big-json": "^3.2.0", "bluebird": "^3.7.2", "chalk": "^4.1.2", - "lodash": "^4.17.23", + "lodash": "4.18.1", "merge": "^2.1.1", "mkdirp": "^1.0.4", "progress-stream": "^2.0.0", @@ -576,9 +576,9 @@ } }, "node_modules/@contentstack/cli-cm-import": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-import/-/cli-cm-import-1.32.0.tgz", - "integrity": "sha512-S+WVN+b5kMa+VaaZSM0oaaYsR3SGTOjlJlvzeFe0Kykkxau6DKbEMvDE2esmsFn21HpUxjLb58//7KS5BhedhQ==", + "version": "1.32.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-import/-/cli-cm-import-1.32.1.tgz", + "integrity": "sha512-WPdKpFq2iYAoCXrMp9ZlahgzhuHbovQqmQSTxvxCBoQZPt8YX2jr8fu6wgzGEhvI8ra4Tpx/br0n2vEMBbSQSg==", "license": "MIT", "dependencies": { "@contentstack/cli-audit": "~1.19.0", @@ -591,7 +591,7 @@ "chalk": "^4.1.2", "debug": "^4.4.3", "fs-extra": "^11.3.3", - "lodash": "^4.17.23", + "lodash": "4.18.1", "marked": "^4.3.0", "merge": "^2.1.1", "mkdirp": "^1.0.4", @@ -604,9 +604,9 @@ } }, "node_modules/@contentstack/cli-cm-import-setup": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-import-setup/-/cli-cm-import-setup-1.8.0.tgz", - "integrity": "sha512-h4vTeFNfKF09NTbJmki1p4EYEh9KxMJn3G7U3CJ7IurcxI6ccNxJuEFhsCHYGwX+UwmZ8TcJMqmbsHwAMQjP0Q==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-import-setup/-/cli-cm-import-setup-1.8.1.tgz", + "integrity": "sha512-xffwa0MXGH8dk+FGOefETnv2LOOyAwKPwG9+QLoLqLxoCRadiMKsTcaw8ejZcB7i1NXbpEp4aWtOkBLOwEc9KA==", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.8.0", @@ -615,7 +615,7 @@ "big-json": "^3.2.0", "chalk": "^4.1.2", "fs-extra": "^11.3.0", - "lodash": "^4.17.23", + "lodash": "4.18.1", "merge": "^2.1.1", "mkdirp": "^1.0.4", "winston": "^3.17.0" @@ -1019,9 +1019,9 @@ } }, "node_modules/@contentstack/cli-config": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-config/-/cli-config-1.20.0.tgz", - "integrity": "sha512-WURtexv9+lQWNPriWvaakHS+9SmGoO3Aq/zLu5SNt2k2Mj+awJwUehYcuZIVflTVzXlUQvxtU0Bn/mCpX2jkmQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-config/-/cli-config-1.20.1.tgz", + "integrity": "sha512-V7t2Nk5BaP1RnTn9gcd3sOAG/r0dagRD1mEIUd9qgxzQuA2f7Uwap09C4sKLP7IKLtAx8tBlFfrzuOoqr7u8sg==", "license": "MIT", "dependencies": { "@contentstack/cli-command": "~1.8.0", @@ -1029,7 +1029,7 @@ "@contentstack/utils": "~1.7.0", "@oclif/core": "^4.8.3", "@oclif/plugin-help": "^6.2.28", - "lodash": "^4.17.23" + "lodash": "^4.18.1" }, "engines": { "node": ">=14.0.0" @@ -1095,9 +1095,9 @@ } }, "node_modules/@contentstack/cli-utilities": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-1.18.0.tgz", - "integrity": "sha512-JEm6ElIegkcibHUEjRF+Id9529bAXBqkf0Givs9GL5CZE7d8eiLzFCUnlb51VZynk1g5+SmjY5nSeghrmcVSPg==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-utilities/-/cli-utilities-1.18.1.tgz", + "integrity": "sha512-1ymPu5HbOXFdDJHJFiwtT1yVNpmDOgMH8qqCeP3kjS7ED1+rz7Q3cWPnJC9FlUfvFeOAyJaJPPQCiYd0lgujtw==", "license": "MIT", "dependencies": { "@contentstack/management": "~1.27.5", @@ -1116,7 +1116,7 @@ "inquirer-search-list": "^1.2.6", "js-yaml": "^4.1.1", "klona": "^2.0.6", - "lodash": "^4.17.23", + "lodash": "^4.18.1", "mkdirp": "^1.0.4", "open": "^8.4.2", "ora": "^5.4.1", @@ -1144,15 +1144,15 @@ } }, "node_modules/@contentstack/cli-variants": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@contentstack/cli-variants/-/cli-variants-1.4.0.tgz", - "integrity": "sha512-avYWCteVVfChz2m/r6VzLAeRKboJjwZVZuQUEONJb0wOeSlFfUC/koYbUaoAtN8v+0vbVx4Z/EkQAaTJIMDbMg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@contentstack/cli-variants/-/cli-variants-1.4.1.tgz", + "integrity": "sha512-iLl1QFeVLIxJGRSbBoTXp3OyfujBj74zj47yzQKo6eSUMBF4Eelb75zFrQlx2gI3UQY9hRX1KnAtqcfRk7jGmg==", "license": "MIT", "dependencies": { "@contentstack/cli-utilities": "~1.18.0", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", - "lodash": "^4.17.23", + "lodash": "4.18.1", "mkdirp": "^1.0.4", "winston": "^3.17.0" } @@ -1581,9 +1581,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -1598,9 +1598,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -1615,9 +1615,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -1632,9 +1632,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -1649,9 +1649,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -1666,9 +1666,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -1683,9 +1683,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -1700,9 +1700,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -1717,9 +1717,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -1734,9 +1734,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -1751,9 +1751,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -1768,9 +1768,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -1785,9 +1785,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -1802,9 +1802,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -1819,9 +1819,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -1836,9 +1836,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -1853,9 +1853,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -1870,9 +1870,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -1887,9 +1887,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -1904,9 +1904,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -1921,9 +1921,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -1938,9 +1938,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -1955,9 +1955,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -1972,9 +1972,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -1989,9 +1989,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -2006,9 +2006,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -2614,9 +2614,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", - "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", + "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", "dev": true, "license": "MIT", "optional": true, @@ -2684,9 +2684,9 @@ } }, "node_modules/@oclif/core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.10.3.tgz", - "integrity": "sha512-0mD8vcrrX5uRsxzvI8tbWmSVGngvZA/Qo6O0ZGvLPAWEauSf5GFniwgirhY0SkszuHwu0S1J1ivj/jHmqtIDuA==", + "version": "4.10.5", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.10.5.tgz", + "integrity": "sha512-qcdCF7NrdWPfme6Kr34wwljRCXbCVpL1WVxiNy0Ep6vbWKjxAjFQwuhqkoyL0yjI+KdwtLcOCGn5z2yzdijc8w==", "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.2", @@ -2699,7 +2699,7 @@ "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", @@ -2713,9 +2713,9 @@ } }, "node_modules/@oclif/plugin-help": { - "version": "6.2.41", - "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.41.tgz", - "integrity": "sha512-oHqpm9a8NnLY9J5yIA+znchB2QCBqDUu5n7XINdZwfbhO6WOUZ2ANww6QN7crhvAKgpN5HK/ELN8Hy96kgLUuA==", + "version": "6.2.44", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.44.tgz", + "integrity": "sha512-x03Se2LtlOOlGfTuuubt5C4Z8NHeR4zKXtVnfycuLU+2VOMu2WpsGy9nbs3nYuInuvsIY1BizjVaTjUz060Sig==", "license": "MIT", "dependencies": { "@oclif/core": "^4" @@ -2725,13 +2725,13 @@ } }, "node_modules/@oclif/plugin-not-found": { - "version": "3.2.78", - "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.78.tgz", - "integrity": "sha512-wFg7rUYUxYsBMl0fEBHOJ+GAO0/3Nwpn4scmkqV3IQdch7+N1ke8qFOzLZal0kpa0wt+Tr/aJvaT8iYccPGZDQ==", + "version": "3.2.80", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.80.tgz", + "integrity": "sha512-yTLjWvR1r/Rd/cO2LxHdMCDoL5sQhBYRUcOMCmxZtWVWhx4rAZ8KVUPDVsb+SvjJDV5ADTDBgt1H52fFx7YWqg==", "license": "MIT", "dependencies": { "@inquirer/prompts": "^7.10.1", - "@oclif/core": "^4.10.3", + "@oclif/core": "^4.10.5", "ansis": "^3.17.0", "fast-levenshtein": "^3.0.0" }, @@ -2740,9 +2740,9 @@ } }, "node_modules/@oclif/plugin-plugins": { - "version": "5.4.59", - "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-5.4.59.tgz", - "integrity": "sha512-W/F3vNwhC3BHmn1o4g92H8kY4rYw9RsgVRm+GDulZg0XqSoseJYCMQell6ajTj8xljrrG0dZSTuEfc4ETwC2VA==", + "version": "5.4.61", + "resolved": "https://registry.npmjs.org/@oclif/plugin-plugins/-/plugin-plugins-5.4.61.tgz", + "integrity": "sha512-FsXYLdXJWucrAzDQ3Q2G/mFGeTaUIsL4o76ayG6qNaF8iq1n2O3YnniCl90RLphJmty2ScGTv2YIniOHt4HHjw==", "license": "MIT", "dependencies": { "@oclif/core": "^4.8.0", @@ -2812,9 +2812,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -2832,9 +2832,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -2849,9 +2849,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -2866,9 +2866,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -2883,9 +2883,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -2900,9 +2900,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -2917,9 +2917,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -2934,9 +2934,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -2951,9 +2951,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -2968,9 +2968,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -2985,9 +2985,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -3002,9 +3002,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -3019,9 +3019,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -3036,9 +3036,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -3046,16 +3046,52 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -3070,9 +3106,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -3087,9 +3123,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, @@ -3566,18 +3602,18 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", - "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", + "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "node_modules/@sinonjs/samsam": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", - "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", @@ -3877,9 +3913,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3896,7 +3932,7 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -3915,7 +3951,7 @@ "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4282,14 +4318,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", - "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.4.tgz", + "integrity": "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.4", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -4303,8 +4339,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.2", - "vitest": "4.1.2" + "@vitest/browser": "4.1.4", + "vitest": "4.1.4" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -4313,16 +4349,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", - "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", + "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -4330,47 +4366,10 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", - "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.2", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/mocker/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", + "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", "dev": true, "license": "MIT", "dependencies": { @@ -4381,13 +4380,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", - "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", + "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.2", + "@vitest/utils": "4.1.4", "pathe": "^2.0.3" }, "funding": { @@ -4395,14 +4394,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", - "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", + "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", - "@vitest/utils": "4.1.2", + "@vitest/pretty-format": "4.1.4", + "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -4411,9 +4410,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", - "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", + "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", "dev": true, "license": "MIT", "funding": { @@ -4421,13 +4420,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", - "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", + "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.2", + "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4451,9 +4450,9 @@ } }, "node_modules/@wordpress/block-serialization-default-parser": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.42.0.tgz", - "integrity": "sha512-XcX6gOeQOuG0RrUqJV1dadPBUi77uhLhpGfQH/s8vmAEGSWqgJAbjWwUKD8RP6wCGY+IE3Gayd5zu48aVRlB4A==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.44.0.tgz", + "integrity": "sha512-XaVZyQskiI/1Ysq9r2VH4sF017mj3Cl1jOI8IXdpKykOe3YZ6WXPN7FwglVJj5y9Qhw0RgpCObXAORI0PTqDpg==", "license": "GPL-2.0-or-later", "engines": { "node": ">=18.12.0", @@ -4847,9 +4846,9 @@ } }, "node_modules/@wordpress/shortcode": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.42.0.tgz", - "integrity": "sha512-vaXEGjis5IqvPtSMYZgrT2zg5HwjePrs5fgWCwYfX5r/uiizfkeOSedpTBSH/FLpQQTMMeFsr22DLcuF0qdyeA==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.44.0.tgz", + "integrity": "sha512-Vh22BIujZdeeoKYsJ3qEineLeqN/5kURcg9OBIWGBCkKAiCktFcdXUsvaehjZ7VDKWfmNP/Hf9SP/Dt9Gyz44w==", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -5006,9 +5005,9 @@ } }, "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", "license": "MIT", "engines": { "node": ">=12.0" @@ -5648,14 +5647,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -6357,7 +6356,7 @@ "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "^2.8.3" + "yaml": "^1.10.0" }, "engines": { "node": ">=10" @@ -6476,7 +6475,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/csv": { @@ -7020,7 +7019,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "^0.6.2" @@ -7055,7 +7054,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7154,9 +7153,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", @@ -7309,9 +7308,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7322,32 +7321,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -7668,12 +7667,12 @@ } }, "node_modules/express-validator": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", - "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", "license": "MIT", "dependencies": { - "lodash": "^4.17.21", + "lodash": "^4.18.1", "validator": "~13.15.23" }, "engines": { @@ -8111,9 +8110,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8442,9 +8441,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -10344,7 +10343,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -11841,9 +11839,9 @@ } }, "node_modules/mysql2": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", - "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.0.tgz", + "integrity": "sha512-4jaJYBObj7FhD3lnZhqX1yDMuZN4mQNz+IolDySDXT7fbozMBpeGQNcuWXKUqo4ahkAEfkjUHPjnwuDI0/6VKw==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.2", @@ -14928,9 +14926,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -15070,9 +15068,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", "dev": true, "funding": [ { @@ -15328,9 +15326,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -15420,7 +15418,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -15444,7 +15442,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", @@ -15709,11 +15707,12 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" @@ -15807,14 +15806,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -15823,21 +15822,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rollup": { @@ -16064,7 +16063,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" @@ -16278,13 +16277,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -16350,16 +16349,15 @@ } }, "node_modules/sinon": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", - "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", + "version": "21.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.1.2.tgz", + "integrity": "sha512-FS6mN+/bx7e2ajpXkEmOcWB6xBzWiuNoAQT18/+a20SS4U7FSYl8Ms7N6VTUxN/1JAjkx7aXp+THMC8xdpp0gA==", "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.1", - "@sinonjs/samsam": "^9.0.3", - "diff": "^8.0.3", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.3.2", + "@sinonjs/samsam": "^10.0.2", + "diff": "^8.0.4" }, "funding": { "type": "opencollective", @@ -16375,18 +16373,6 @@ "node": ">=0.3.1" } }, - "node_modules/sinon/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -16702,9 +16688,9 @@ } }, "node_modules/std-env": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -17017,7 +17003,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, "license": "MIT" }, "node_modules/thirty-two": { @@ -17065,9 +17050,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, "license": "MIT", "engines": { @@ -17075,13 +17060,13 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -17417,7 +17402,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -17446,12 +17430,12 @@ } }, "node_modules/undici": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", - "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.1.0.tgz", + "integrity": "sha512-E9MkTS4xXLnRPYqxH2e6Hr2/49e7WFDKczKcCaFH4VaZs2iNvHMqeIkyUAD9vM8kujy9TjVrRlQ5KkdEJxB2pw==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { @@ -17605,9 +17589,9 @@ } }, "node_modules/validator": { - "version": "13.15.26", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", - "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -17622,17 +17606,144 @@ "node": ">= 0.8" } }, - "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "node_modules/vitest": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", + "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.4", + "@vitest/mocker": "4.1.4", + "@vitest/pretty-format": "4.1.4", + "@vitest/runner": "4.1.4", + "@vitest/snapshot": "4.1.4", + "@vitest/spy": "4.1.4", + "@vitest/utils": "4.1.4", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.4", + "@vitest/browser-preview": "4.1.4", + "@vitest/browser-webdriverio": "4.1.4", + "@vitest/coverage-istanbul": "4.1.4", + "@vitest/coverage-v8": "4.1.4", + "@vitest/ui": "4.1.4", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/mocker": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", + "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -17650,7 +17761,7 @@ "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", @@ -17700,86 +17811,22 @@ } } }, - "node_modules/vitest": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", - "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "node_modules/vitest/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.2", - "@vitest/mocker": "4.1.2", - "@vitest/pretty-format": "4.1.2", - "@vitest/runner": "4.1.2", - "@vitest/snapshot": "4.1.2", - "@vitest/spy": "4.1.2", - "@vitest/utils": "4.1.2", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, + "license": "ISC", + "optional": true, + "peer": true, "bin": { - "vitest": "vitest.mjs" + "yaml": "bin.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 14.6" }, "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.2", - "@vitest/browser-preview": "4.1.2", - "@vitest/browser-webdriverio": "4.1.2", - "@vitest/ui": "4.1.2", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/void-elements": { @@ -18163,19 +18210,13 @@ } }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" + "node": ">= 6" } }, "node_modules/yargs": { diff --git a/api/package.json b/api/package.json index 4f98711e3..d75f4be29 100644 --- a/api/package.json +++ b/api/package.json @@ -34,6 +34,9 @@ "@contentstack/cli-utilities": "^1.17.1", "@contentstack/json-rte-serializer": "^3.0.5", "@contentstack/marketplace-sdk": "^1.5.0", + "@emnapi/core": "1.9.1", + "@emnapi/runtime": "1.9.1", + "@emnapi/wasi-threads": "1.2.0", "@wordpress/block-serialization-default-parser": "^5.39.0", "axios": "^1.15.0", "cheerio": "^1.2.0", @@ -59,10 +62,7 @@ "php-serialize": "^5.1.3", "socket.io": "^4.7.5", "uuid": "^9.0.1", - "winston": "^3.11.0", - "@emnapi/core": "1.9.1", - "@emnapi/runtime" : "1.9.1", - "@emnapi/wasi-threads": "1.2.0" + "winston": "^3.11.0" }, "devDependencies": { "@types/cors": "^2.8.17", diff --git a/api/src/services/contentMapper.service.ts b/api/src/services/contentMapper.service.ts index f457fcd09..a3974610b 100644 --- a/api/src/services/contentMapper.service.ts +++ b/api/src/services/contentMapper.service.ts @@ -1521,6 +1521,54 @@ const getExistingTaxonomies = async (req: Request) => { ); } } + + // Path 3: Contentful export validation (upload-api contentfulMigrationData) + if (sourceTaxonomies.length === 0) { + try { + const contentfulTaxonomyPath = path.join( + process.cwd(), + '..', + 'upload-api', + 'contentfulMigrationData', + 'taxonomySchema', + 'taxonomySchema.json', + ); + const resolvedCf = path.resolve(contentfulTaxonomyPath); + if ( + resolvedCf.includes('upload-api') && + resolvedCf.includes('contentfulMigrationData') + ) { + const stats = await fs.promises + .lstat(resolvedCf) + .catch(() => null); + if (stats && stats.isFile() && !stats.isSymbolicLink()) { + const taxonomyData = await fs.promises.readFile( + resolvedCf, + 'utf8', + ); + const taxonomiesArray = JSON.parse(taxonomyData); + const cfTaxonomies = ( + Array.isArray(taxonomiesArray) + ? taxonomiesArray + : Object.values(taxonomiesArray) + ).map((taxonomy: any) => ({ + uid: taxonomy.uid || '', + name: taxonomy.name || taxonomy.uid || '', + description: taxonomy.description || '', + source: 'source_cms', + })); + sourceTaxonomies.push(...cfTaxonomies); + logger.info( + `Found ${cfTaxonomies.length} taxonomies from upload-api contentfulMigrationData`, + ); + } + } + } catch (cfTaxError: any) { + logger.warn( + `Could not read Contentful taxonomies from upload-api: ${cfTaxError.message}`, + ); + } + } } // Step 2: Get destination taxonomies from Contentstack (if stack exists and token_payload is available) diff --git a/api/src/services/contentful.service.ts b/api/src/services/contentful.service.ts index 37af61b8b..d5ecc13f6 100644 --- a/api/src/services/contentful.service.ts +++ b/api/src/services/contentful.service.ts @@ -10,6 +10,11 @@ import { jsonToHtml, jsonToMarkdown, htmlToJson } from '@contentstack/json-rte-s import { CHUNK_SIZE, LOCALE_MAPPER, MIGRATION_DATA_CONFIG } from "../constants/index.js"; import { Locale } from "../models/types.js"; import jsonRTE from "./contentful/jsonRTE.js"; +import { + buildContentfulTaxonomyAssignments, + contentfulSchemeIdToStackTaxonomyUid, + createTaxonomy as createContentfulTaxonomyFromExport, +} from "./contentful/taxonomy.service.js"; import { getAllLocales, getLogMessage } from "../utils/index.js"; import customLogger from "../utils/custom-logger.utils.js"; @@ -99,6 +104,52 @@ const mapLocales = ({ masterLocale, locale, locales, isNull = false }: any) => { } } +/** + * When an entry has `metadata.concepts` but no field locales, choose Contentful locale key(s) + * that align with the project/package locale mapper so `mapLocales` can resolve them later. + */ +function pickContentfulLocaleFromMasterLocaleMap(master: unknown): string | undefined { + if (!master || typeof master !== 'object' || Array.isArray(master)) return undefined; + const m = master as Record; + const keys = Object.keys(m); + if (!keys.length) return undefined; + for (const k of keys) { + if (k.includes('-')) return k; + } + for (const v of Object.values(m)) { + if (typeof v === 'string' && v.includes('-')) return v; + } + return keys[0]; +} + +function resolveLocalesForTaxonomyMetadata( + entryLocaleKeys: Set, + entryDataBranch: Record | undefined, + localeMapper: Record, + entrySysLocale?: string, +): string[] { + const fromFields = [...entryLocaleKeys]; + if (fromFields.length) return fromFields; + + if (entrySysLocale && typeof entrySysLocale === 'string') { + return [entrySysLocale]; + } + + const fromExisting = Object.keys(entryDataBranch || {}); + if (fromExisting.length) return fromExisting; + + const fromProjectMaster = pickContentfulLocaleFromMasterLocaleMap(localeMapper?.masterLocale); + if (fromProjectMaster) return [fromProjectMaster]; + + const fromDefaultMaster = pickContentfulLocaleFromMasterLocaleMap(LOCALE_MAPPER?.masterLocale); + if (fromDefaultMaster) return [fromDefaultMaster]; + + const otherKeys = Object.keys(localeMapper || {}).filter((k) => k !== 'masterLocale'); + if (otherKeys.length) return [otherKeys[0]]; + + return ['en-US']; +} + function resolveEntryFieldKey(entry: Record, baseKey: string): string | undefined { if (baseKey in entry) return baseKey; const snake = baseKey.replace(/([A-Z])/g, (m) => `_${m.toLowerCase()}`); @@ -106,6 +157,20 @@ function resolveEntryFieldKey(entry: Record, baseKey: string): return undefined; } +/** Allowed taxonomy scheme UIDs from Contentful export content type `metadata.taxonomy` (sanitized for Contentstack). */ +function getAllowedTaxonomySchemesFromExportContentType( + contentTypesFromPackage: any[] | undefined, + contentTypeId: string, +): string[] { + if (!contentTypesFromPackage?.length) return []; + const ctDef = contentTypesFromPackage.find((c: any) => c?.sys?.id === contentTypeId); + const links = ctDef?.metadata?.taxonomy; + if (!Array.isArray(links)) return []; + return links + .map((l: any) => contentfulSchemeIdToStackTaxonomyUid(l?.sys?.id)) + .filter(Boolean); +} + /** * Maps Contentful content type id → field id → whether that field is localized in the export schema. * Used so we only fan out values for fields with `localized: false`, not for localized fields that @@ -129,6 +194,85 @@ function buildContentfulFieldLocalizedByContentType( return byCt; } +/** + * When the export omits `widgetId`, infer defaults aligned with + * upload-api/migration-contentful/libs/contentTypeMapper.js. + */ +function inferContentfulDefaultWidgetId(fieldType: string | undefined): string | undefined { + switch (fieldType) { + case "Symbol": + return "singleLine"; + case "Text": + return "multipleLine"; + case "Integer": + case "Number": + return "numberEditor"; + case "RichText": + return "richTextEditor"; + case "Boolean": + return "boolean"; + default: + return undefined; + } +} + +function getContentfulFieldFromPackage( + contentTypesFromPackage: any[] | undefined, + ctId: string, + fieldId: string +): any | undefined { + const ct = contentTypesFromPackage?.find((c: any) => c?.sys?.id === ctId); + return ct?.fields?.find((f: any) => f?.id === fieldId); +} + +/** + * Picks one fieldMapping row when several share the same `uid` (e.g. bootstrap `title`/`url` rows + * from createInitialMapper plus the real Contentful field). Mapper `otherCmsType` is Contentful + * `widgetId` from the migration pipeline. + */ +function resolveFieldMappingRow( + fieldMapping: any[] | undefined, + contentTypesFromPackage: any[] | undefined, + ctId: string, + fieldId: string +): any | undefined { + const candidates = fieldMapping?.filter((item: any) => item?.uid === fieldId) ?? []; + if (candidates?.length === 0) return undefined; + if (candidates?.length === 1) return candidates?.[0]; + + const cfField = getContentfulFieldFromPackage(contentTypesFromPackage, ctId, fieldId); + const widgetId = cfField?.widgetId ?? inferContentfulDefaultWidgetId(cfField?.type); + if (widgetId) { + const byWidget = candidates?.filter((c: any) => c?.otherCmsType === widgetId); + if (byWidget?.length >= 1) return byWidget?.[0]; + } + + const typeToCs: Record = { + RichText: "json", + Boolean: "boolean", + Date: "isodate", + }; + const expectCs = cfField?.type ? typeToCs[cfField?.type as string] : undefined; + if (expectCs) { + const byCs = candidates?.filter((c: any) => c?.contentstackFieldType === expectCs); + if (byCs?.length >= 1) return byCs?.[0]; + } + + if (cfField?.type === "Boolean") { + const byBool = candidates?.filter((c: any) => c?.contentstackFieldType === "boolean"); + if (byBool?.length >= 1) return byBool?.[0]; + } + + // Legacy bootstrap rows use otherCmsType "text" while real Symbol/Text fields use widget ids + // (e.g. singleLine). Prefer non-"text" otherCmsType when the schema is Symbol/Text. + if (cfField && ["Symbol", "Text"]?.includes(cfField.type)) { + const nonBootstrap = candidates?.filter((c: any) => c?.otherCmsType !== "text"); + if (nonBootstrap?.length >= 1) return nonBootstrap[0]; + } + + return candidates?.[0]; +} + const transformCloudinaryObject = (input: any) => { const result: any = []; if (!Array.isArray(input)) { @@ -828,24 +972,32 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje { sys: { id, + locale: entrySysLocale, contentType: { sys: { id: name }, }, environment: { sys: { id: environment_id = "" } = {} } = {}, }, fields, + metadata, }: any ) => { entryData[name] ??= {}; + const currentCT = contentTypes?.find((ct: any) => ct?.otherCmsUid === name); - Object.entries(fields).forEach(([key, value]) => { - const currentCT = contentTypes?.find((ct: any) => ct?.otherCmsUid === name); + Object.entries(fields || {}).forEach(([key, value]) => { const locales: string[] = []; Object.entries(value as object).forEach(([lang, langValue]) => { entryData[name][lang] ??= {}; entryData[name][lang][id] ??= {}; locales.push(lang); - const fieldData = currentCT?.fieldMapping?.find?.((item: any) => key === item?.uid); + + const fieldData = resolveFieldMappingRow( + currentCT?.fieldMapping, + content, + name, + key + ); const newId = fieldData?.contentstackFieldUid ?? `${key}`?.replace?.(/[^a-zA-Z0-9]+/g, "_"); entryData[name][lang][id][newId] = processField( langValue, @@ -856,6 +1008,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje fieldData ); }); + const pathName = getDisplayName(name, displayField); locales.forEach((locale) => { const localeCode = mapLocales({ masterLocale: master_locale, locale, locales: LocaleMapper }); @@ -897,16 +1050,15 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje // Do not infer non-localized-ness from a single locale key — localized fields can legitimately // have only one locale when translations are missing. const entryLocaleKeys = new Set(); - for (const [, v] of Object?.entries?.(fields)) { + for (const [, v] of Object?.entries?.(fields || {})) { for (const lang of Object.keys(v as object)) { entryLocaleKeys.add(lang); } } - const ct = contentTypes?.find((c: any) => c?.otherCmsUid === name); - for (const [key, value] of Object?.entries?.(fields)) { + for (const [key, value] of Object?.entries?.(fields || {})) { const langs = Object?.keys(value as object); if (langs?.length !== 1) continue; - const fd = ct?.fieldMapping?.find?.((item: any) => key === item?.uid); + const fd = resolveFieldMappingRow(currentCT?.fieldMapping, content, name, key); const localizedInCf = cfFieldLocalizedByCt.get(name)?.get(key); const explicitlyNonLocalized = localizedInCf === false || @@ -929,6 +1081,51 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje } } + const metaTaxField = currentCT?.fieldMapping?.find( + (f: any) => + f?.otherCmsType === 'TaxonomyMetadata' || + f?.contentstackFieldType === 'taxonomy' || + f?.contentstackFieldUid === 'taxonomies' || + f?.contentstackFieldUid === 'metadata_taxonomies', + ); + let allowedFromMapper: string[] = []; + if (metaTaxField) { + const taxonomiesConfig = + metaTaxField?.advanced?.taxonomies || metaTaxField?.taxonomies || []; + allowedFromMapper = taxonomiesConfig + .map((t: any) => (typeof t === 'string' ? t : t?.taxonomy_uid)) + .filter(Boolean) + .map((uid: string) => contentfulSchemeIdToStackTaxonomyUid(uid)) + .filter(Boolean); + } + const allowedFromExport = getAllowedTaxonomySchemesFromExportContentType( + content, + name, + ); + const allowedSchemes = + allowedFromMapper.length > 0 ? allowedFromMapper : allowedFromExport; + + if (metadata?.concepts?.length) { + const taxValue = buildContentfulTaxonomyAssignments( + metadata.concepts, + allowedSchemes, + ); + if (taxValue.length) { + const fieldKey = metaTaxField?.contentstackFieldUid || 'taxonomies'; + const localesForTax = resolveLocalesForTaxonomyMetadata( + entryLocaleKeys, + entryData[name], + LocaleMapper, + entrySysLocale, + ); + for (const loc of localesForTax) { + entryData[name][loc] ??= {}; + entryData[name][loc][id] ??= {}; + entryData[name][loc][id][fieldKey] = taxValue; + } + } + } + return entryData; }, {} @@ -1468,4 +1665,5 @@ export const contentfulService = { createRefrence, createWebhooks, createVersionFile, + createTaxonomy: createContentfulTaxonomyFromExport, }; diff --git a/api/src/services/contentful/taxonomy.service.ts b/api/src/services/contentful/taxonomy.service.ts new file mode 100644 index 000000000..e93a0f855 --- /dev/null +++ b/api/src/services/contentful/taxonomy.service.ts @@ -0,0 +1,237 @@ +import fs from 'fs'; +import path from 'path'; +import { getLogMessage } from '../../utils/index.js'; +import customLogger from '../../utils/custom-logger.utils.js'; +import { MIGRATION_DATA_CONFIG } from '../../constants/index.js'; + +const { DATA, TAXONOMIES_DIR_NAME, TAXONOMIES_FILE_NAME } = MIGRATION_DATA_CONFIG; + +/** + * Contentful export uses scheme ids like `productCategory`. Contentstack taxonomy UIDs must be + * lowercase alphanumeric + underscores only (no camelCase). + */ +export function contentfulSchemeIdToStackTaxonomyUid(contentfulSchemeId: string): string { + if (!contentfulSchemeId || typeof contentfulSchemeId !== 'string') return ''; + return contentfulSchemeId + .replace(/([A-Z])/g, '_$1') + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + +/** Maps Contentful concept id prefix (before first "-") to Contentstack taxonomy uid (sanitized). */ +export const CONCEPT_PREFIX_TO_SCHEME: Record = { + brd: 'brand', + cat: 'product_category', + branch: 'branch', + dis: 'discipline', +}; + +export function inferSchemeFromConceptId(conceptId: string): string | null { + if (!conceptId || typeof conceptId !== 'string') return null; + const prefix = conceptId.split('-')[0]; + return CONCEPT_PREFIX_TO_SCHEME[prefix] ?? null; +} + +export function sanitizeTermUid(conceptId: string): string { + return conceptId + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + +function humanizeSchemeId(id: string): string { + if (!id) return ''; + const words = id.split('_').filter(Boolean); + return words + .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) + .join(' '); +} + +export function buildContentfulTaxonomyAssignments( + concepts: Array<{ sys?: { id?: string } }> | undefined, + allowedSchemeIds: string[], +): Array<{ taxonomy_uid: string; term_uid: string }> { + const allow = (allowedSchemeIds || []).filter(Boolean); + const allowSet = new Set( + allow.map((id) => contentfulSchemeIdToStackTaxonomyUid(id)).filter(Boolean), + ); + const useAllow = allowSet.size > 0; + const out: Array<{ taxonomy_uid: string; term_uid: string }> = []; + const seen = new Set(); + + for (const c of concepts || []) { + const id = c?.sys?.id; + if (!id) continue; + const scheme = inferSchemeFromConceptId(id); + if (!scheme) continue; + if (useAllow && !allowSet.has(scheme)) continue; + const termUid = sanitizeTermUid(id); + const key = `${scheme}::${termUid}`; + if (seen.has(key)) continue; + seen.add(key); + out.push({ taxonomy_uid: scheme, term_uid: termUid }); + } + return out; +} + +interface TaxonomyTerm { + uid: string; + name: string; + parent_uid: string | null; + description?: string; + contentful_concept_id: string; +} + +interface TaxonomyStructure { + taxonomy: { + uid: string; + name: string; + description: string; + }; + terms: TaxonomyTerm[]; +} + +const saveTaxonomyFiles = async ( + taxonomies: Record, + taxonomiesPath: string, + projectId: string, + destination_stack_id: string, +): Promise => { + for (const [schemeUid, taxonomy] of Object.entries(taxonomies)) { + const filePath = path.join(taxonomiesPath, `${schemeUid}.json`); + await fs.promises.writeFile(filePath, JSON.stringify(taxonomy, null, 2), 'utf8'); + const message = getLogMessage( + 'saveTaxonomyFiles', + `Saved taxonomy file: ${schemeUid}.json with ${taxonomy.terms.length} terms.`, + {}, + ); + await customLogger(projectId, destination_stack_id, 'info', message); + } + + const taxonomiesDataObject: Record = {}; + for (const [schemeUid, taxonomy] of Object.entries(taxonomies)) { + taxonomiesDataObject[schemeUid] = { + uid: taxonomy.taxonomy.uid, + name: taxonomy.taxonomy.name, + description: taxonomy.taxonomy.description, + }; + } + + const taxonomiesFilePath = path.join(taxonomiesPath, TAXONOMIES_FILE_NAME); + await fs.promises.writeFile( + taxonomiesFilePath, + JSON.stringify(taxonomiesDataObject, null, 2), + 'utf8', + ); + await customLogger( + projectId, + destination_stack_id, + 'info', + getLogMessage( + 'saveTaxonomyFiles', + `Saved consolidated ${TAXONOMIES_FILE_NAME} with ${Object.keys(taxonomiesDataObject).length} taxonomies.`, + {}, + ), + ); +}; + +/** + * Builds taxonomy vocabularies and terms from a Contentful export JSON (metadata.taxonomy on content types, + * metadata.concepts on entries) and writes the same layout as Drupal: per-scheme JSON + taxonomies.json. + */ +export const createTaxonomy = async ( + packagePath: string, + destination_stack_id: string, + projectId: string, +): Promise => { + const taxonomiesPath = path.join(DATA, destination_stack_id, TAXONOMIES_DIR_NAME); + + try { + await fs.promises.mkdir(taxonomiesPath, { recursive: true }); + const raw = await fs.promises.readFile(packagePath, 'utf8'); + const data = JSON.parse(raw); + const contentTypes = data?.contentTypes || []; + const entries = data?.entries || []; + + const schemeIds = new Set(); + for (const ct of contentTypes) { + for (const link of ct?.metadata?.taxonomy || []) { + const sid = link?.sys?.id; + if (sid) schemeIds.add(contentfulSchemeIdToStackTaxonomyUid(sid)); + } + } + + const termsByScheme: Record> = {}; + for (const sid of schemeIds) { + termsByScheme[sid] = new Map(); + } + + for (const entry of entries) { + for (const c of entry?.metadata?.concepts || []) { + const conceptId = c?.sys?.id; + if (!conceptId) continue; + const scheme = inferSchemeFromConceptId(conceptId); + if (!scheme || !termsByScheme[scheme]) continue; + const termUid = sanitizeTermUid(conceptId); + if (!termsByScheme[scheme].has(termUid)) { + termsByScheme[scheme].set(termUid, conceptId); + } + } + } + + const taxonomies: Record = {}; + + for (const schemeUid of schemeIds) { + const termMap = termsByScheme[schemeUid]; + const terms: TaxonomyTerm[] = []; + for (const [termUid, conceptId] of termMap) { + terms.push({ + uid: termUid, + name: conceptId, + parent_uid: null, + description: '', + contentful_concept_id: conceptId, + }); + } + taxonomies[schemeUid] = { + taxonomy: { + uid: schemeUid, + name: humanizeSchemeId(schemeUid) || schemeUid, + description: 'Imported from Contentful taxonomy', + }, + terms, + }; + } + + if (Object.keys(taxonomies).length === 0) { + const message = getLogMessage( + 'createTaxonomy', + 'No Contentful taxonomy schemes found on content types (metadata.taxonomy). Skipping taxonomy files.', + {}, + ); + await customLogger(projectId, destination_stack_id, 'info', message); + return; + } + + await saveTaxonomyFiles(taxonomies, taxonomiesPath, projectId, destination_stack_id); + + const successMessage = getLogMessage( + 'createTaxonomy', + `Exported ${Object.keys(taxonomies).length} Contentful taxonomies.`, + {}, + ); + await customLogger(projectId, destination_stack_id, 'info', successMessage); + } catch (err) { + const message = getLogMessage( + 'createTaxonomy', + 'Error encountered while creating taxonomies from Contentful export.', + {}, + err, + ); + await customLogger(projectId, destination_stack_id, 'error', message); + throw err; + } +}; diff --git a/api/src/services/migration.service.ts b/api/src/services/migration.service.ts index b43b245a7..9a5494a3f 100644 --- a/api/src/services/migration.service.ts +++ b/api/src/services/migration.service.ts @@ -515,6 +515,11 @@ const startTestMigration = async (req: Request): Promise => { projectId, true ); + await contentfulService?.createTaxonomy( + cleanLocalPath, + project?.current_test_stack_id, + projectId, + ); await contentfulService?.createEntry( cleanLocalPath, project?.current_test_stack_id, @@ -925,6 +930,11 @@ const startMigration = async (req: Request): Promise => { project?.destination_stack_id, projectId ); + await contentfulService?.createTaxonomy( + cleanLocalPath, + project?.destination_stack_id, + projectId, + ); await contentfulService?.createEntry( cleanLocalPath, project?.destination_stack_id, diff --git a/api/src/services/wordpress.service.ts b/api/src/services/wordpress.service.ts index bfaac3f37..628c980a4 100644 --- a/api/src/services/wordpress.service.ts +++ b/api/src/services/wordpress.service.ts @@ -1638,11 +1638,11 @@ function getAuthorFieldValue(field: any, authorData: any, fallbackUrl?: string): const fieldMapping: Record = { 'email': 'wp:author_email', 'first_name': 'wp:author_first_name', - 'firstname': 'wp:author_first_name', + 'first name': 'wp:author_first_name', 'last_name': 'wp:author_last_name', - 'lastname': 'wp:author_last_name', + 'last name': 'wp:author_last_name', 'display_name': 'wp:author_display_name', - 'displayname': 'wp:author_display_name', + 'display name': 'wp:author_display_name', 'description': 'wp:author_description', 'website': 'wp:author_url', 'url': 'wp:author_url', diff --git a/api/src/utils/content-type-creator.utils.ts b/api/src/utils/content-type-creator.utils.ts index 1b74ff6fc..ddcdd3eda 100644 --- a/api/src/utils/content-type-creator.utils.ts +++ b/api/src/utils/content-type-creator.utils.ts @@ -8,7 +8,7 @@ import customLogger from './custom-logger.utils.js'; import { getLogMessage } from './index.js'; import { LIST_EXTENSION_UID, MIGRATION_DATA_CONFIG } from '../constants/index.js'; import { contentMapperService } from "../services/contentMapper.service.js"; -import appMeta from '../constants/app/index.json' with { type: 'json' }; +import appMeta from '../constants/app/index.json'; const { GLOBAL_FIELDS_FILE_NAME, @@ -39,6 +39,17 @@ interface ContentType { const RESERVED_UIDS = new Set(['locale', 'publish_details', 'tags']); +/** Contentful taxonomy scheme ids may be camelCase, Contentstack requires [a-z0-9_]. */ +function normalizeStackTaxonomyUid(raw?: string): string { + if (!raw || typeof raw !== 'string') return ''; + return raw + .replace(/([A-Z])/g, '_$1') + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + function sanitizeUid(uid?: string) { if (!uid) return uid; let out = uid?.replace?.(/[^a-zA-Z0-9_]/g, '_').replace?.(/^_+/, ''); @@ -116,11 +127,11 @@ const uidCorrector = ({ uid } : {uid : string}) => { * issues do not go unnoticed. * @returns The remapped UIDs. */ -function remapReferenceUids(uids: string[], keyMapper?: Record): string[] { - if (!keyMapper || !Object.keys(keyMapper).length) return uids; - return uids.map(uid => keyMapper[uid] ?? keyMapper[uidCorrector({ uid })] ?? uid); +function remapReferenceUids(uids: string | string[], keyMapper?: Record): string[] { + const uidsArray = Array.isArray(uids) ? uids : [uids]; + if (!keyMapper || !Object.keys(keyMapper).length) return uidsArray; + return uidsArray?.map(uid => keyMapper?.[uid] ?? keyMapper?.[uidCorrector({ uid })] ?? uid); } - function buildFieldSchema(item: any, marketPlacePath: string, parentUid = '', keyMapper?: Record): any { if (item?.isDeleted === true) return null; @@ -790,17 +801,19 @@ export const convertToSchemaFormate = ({ field, advanced = false, marketPlacePat const taxonomiesData = field?.taxonomies || field?.advanced?.taxonomies || []; const taxonomiesArray = Array.isArray(taxonomiesData) ? taxonomiesData.map((tax: any) => ({ - taxonomy_uid: typeof tax === 'string' ? tax : (tax?.taxonomy_uid || tax), + taxonomy_uid: normalizeStackTaxonomyUid( + typeof tax === 'string' ? tax : (tax?.taxonomy_uid || tax), + ), mandatory: field?.advanced?.mandatory ?? false, multiple: field?.advanced?.multiple !== false, // Default true for taxonomies - non_localizable: field?.advanced?.nonLocalizable ?? false + non_localizable: false })) : []; return { data_type: "taxonomy", display_name: field?.title, - uid: cleanedUid, + uid: 'taxonomies', taxonomies: taxonomiesArray, field_metadata: { description: field?.advanced?.description ?? '', @@ -812,7 +825,7 @@ export const convertToSchemaFormate = ({ field, advanced = false, marketPlacePat }, mandatory: field?.advanced?.mandatory ?? false, multiple: field?.advanced?.multiple !== false, // Default true for taxonomies - non_localizable: field?.advanced?.nonLocalizable ?? false, + non_localizable: false, unique: field?.advanced?.unique ?? false }; } @@ -1044,10 +1057,37 @@ const resolveIsSsoFlag = (is_sso: any): boolean => { ); }; +/** + * Resolves the Contentstack Management API UID for a content type / global field. + * @param migrationContentstackUid - The UID of the content type in the migration data. + * @param keyMapper - The key mapper object. + * @returns The Contentstack Management API UID. + */ +function resolveStackContentTypeUid( + migrationContentstackUid: string, + keyMapper?: Record, +): string { + const mapped = keyMapper?.[migrationContentstackUid]; + if (mapped === undefined || mapped === null || mapped === '') { + return migrationContentstackUid; + } + + const m = String(mapped).trim(); + const looksLikeUuid = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(m); + const looksLikeMongoId = /^[0-9a-f]{24}$/i.test(m); + + if (looksLikeUuid || looksLikeMongoId) { + return migrationContentstackUid; + } + + return m; +} + const existingCtMapper = async ({ keyMapper, contentTypeUid, projectId, region, user_id, is_sso, type}: any) => { try { const normalizedIsSso = resolveIsSsoFlag(is_sso); - const ctUid = keyMapper?.[contentTypeUid]; + const ctUid = resolveStackContentTypeUid(contentTypeUid, keyMapper); if(type === 'global_field') { @@ -1102,6 +1142,92 @@ const mergeArrays = async (a: any[], b: any[]) => { return a; } +/** + * Clones a schema branch. + * @param node - The node to clone. + * @returns The cloned node. + */ +function cloneSchemaBranch(node: any): any { + if (node === undefined || node === null) return node; + try { + return structuredClone(node); + } catch { + return JSON.parse(JSON.stringify(node)); + } +} + +/** + * Finds the target modular blocks field. + * @param field - The field to find the target modular blocks field for. + * @param targetSchema - The target schema. + * @returns The target modular blocks field. + */ +function findTargetModularBlocksField(field: any, targetSchema: any[]): any | undefined { + if (!Array.isArray(targetSchema) || !field || field?.data_type !== 'blocks') return undefined; + + const byUid = targetSchema.find( + (mb: any) => mb?.data_type === 'blocks' && mb?.uid === field?.uid, + ); + if (byUid) return byUid; + + const fd = (field?.display_name ?? '').toString().trim().toLowerCase(); + if (fd) { + const byName = targetSchema.find( + (mb: any) => + mb?.data_type === 'blocks' && + (mb?.display_name ?? '').toString().trim().toLowerCase() === fd, + ); + if (byName) return byName; + } + + return undefined; +} + +/** + * Merge modular blocks preserving destination block order and UIDs: + * 1. Walk destination blocks — merge matching source blocks, clone unmapped ones. + * 2. Append source-only blocks (uids not on destination) at the end. + */ +function mergeModularBlocksFieldFromDestination(field: any, targetMB: any) { + const targetBlocks = targetMB?.blocks ?? []; + const sourceBlocks = field?.blocks ?? []; + if (!targetBlocks.length) return; + + const resultBlocks: any[] = []; + const matchedSourceUids = new Set(); + + for (const tb of targetBlocks) { + const sb = sourceBlocks.find((b: any) => b?.uid === tb?.uid); + if (sb) { + const tSch = tb?.schema ?? []; + const additional = tSch.filter( + (tField: any) => + !(sb?.schema ?? []).some( + (sField: any) => + sField?.uid === tField?.uid && sField?.data_type === tField?.data_type, + ), + ); + sb.schema = removeDuplicateFields([ + ...(sb?.schema ?? []), + ...additional.map((f: any) => cloneSchemaBranch(f)), + ]); + mergeSchemaFields(sb?.schema ?? [], tSch); + resultBlocks.push(sb); + if (sb?.uid) matchedSourceUids.add(sb?.uid); + } else { + resultBlocks.push(cloneSchemaBranch(tb)); + } + } + + for (const sb of sourceBlocks) { + if (sb?.uid && !matchedSourceUids.has(sb?.uid)) { + resultBlocks.push(sb); + } + } + + field.blocks = removeDuplicateFields(resultBlocks); +} + function mergeSchemaFields(sourceSchema: any[], targetSchema: any[]) { for (const field of sourceSchema) { if (field?.data_type === 'group') { @@ -1119,27 +1245,10 @@ function mergeSchemaFields(sourceSchema: any[], targetSchema: any[]) { } if (field?.data_type === 'blocks') { - const targetMB = targetSchema?.find((mb: any) => - mb?.uid === field?.uid && mb?.data_type === 'blocks' - ); + const targetMB = findTargetModularBlocksField(field, targetSchema ?? []); - if (targetMB?.blocks) { - for (const sourceBlock of field?.blocks ?? []) { - const targetBlock = targetMB?.blocks?.find((tb: any) => tb?.uid === sourceBlock?.uid); - - if (targetBlock?.schema) { - const additional = (targetBlock?.schema ?? [])?.filter((tField: any) => - !sourceBlock?.schema?.find((sField: any) => sField?.uid === tField?.uid && sField?.data_type === tField?.data_type) - ); - sourceBlock.schema = removeDuplicateFields([...sourceBlock?.schema ?? [], ...additional]); - mergeSchemaFields(sourceBlock.schema, targetBlock.schema ?? []); - } - } - - const additionalBlocks = (targetMB?.blocks ?? []).filter((tb: any) => - !field?.blocks?.find((sb: any) => sb?.uid === tb?.uid) - ); - field.blocks = removeDuplicateFields([...field?.blocks ?? [], ...additionalBlocks]); + if (targetMB?.blocks?.length) { + mergeModularBlocksFieldFromDestination(field, targetMB); } } } diff --git a/api/tests/unit/services/migration.service.test.ts b/api/tests/unit/services/migration.service.test.ts index e72ebc7b6..e3b25473f 100644 --- a/api/tests/unit/services/migration.service.test.ts +++ b/api/tests/unit/services/migration.service.test.ts @@ -105,6 +105,7 @@ vi.mock('../../../src/services/contentful.service.js', () => ({ createRefrence: vi.fn().mockResolvedValue(undefined), createWebhooks: vi.fn().mockResolvedValue(undefined), createEnvironment: vi.fn().mockResolvedValue(undefined), + createTaxonomy: vi.fn().mockResolvedValue(undefined), createAssets: vi.fn().mockResolvedValue(undefined), createEntry: vi.fn().mockResolvedValue(undefined), createVersionFile: vi.fn().mockResolvedValue(undefined), diff --git a/api/tests/unit/services/user.service.test.ts b/api/tests/unit/services/user.service.test.ts index 0063b7bf7..c619016d2 100644 --- a/api/tests/unit/services/user.service.test.ts +++ b/api/tests/unit/services/user.service.test.ts @@ -1,10 +1,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -const { mockHttps, mockAuthModelRead, mockChainValue } = vi.hoisted(() => ({ - mockHttps: vi.fn(), - mockAuthModelRead: vi.fn(), - mockChainValue: vi.fn(), -})); +const { mockHttps, mockAuthModelRead, mockChainValue, mockRequestWithSsoTokenRefresh } = + vi.hoisted(() => ({ + mockHttps: vi.fn(), + mockAuthModelRead: vi.fn(), + mockChainValue: vi.fn(), + mockRequestWithSsoTokenRefresh: vi.fn(), + })); vi.mock('../../../src/utils/https.utils.js', () => ({ default: mockHttps })); vi.mock('../../../src/utils/logger.js', () => ({ @@ -24,11 +26,26 @@ vi.mock('../../../src/models/authentication.js', () => ({ }), }, data: { - users: [{ user_id: 'user-123', region: 'NA', authtoken: 'cs-token' }], + users: [ + { + user_id: 'user-123', + region: 'NA', + authtoken: 'cs-token', + access_token: 'sso-access-token', + }, + ], }, }, })); +vi.mock('../../../src/utils/auth.utils.js', () => ({ + getAppOrganization: vi.fn(() => ({ uid: 'org-1', name: 'Test Org' })), +})); +vi.mock('../../../src/utils/sso-request.utils.js', () => ({ + requestWithSsoTokenRefresh: mockRequestWithSsoTokenRefresh, +})); +import AuthenticationModel from '../../../src/models/authentication.js'; +import { getAppOrganization } from '../../../src/utils/auth.utils.js'; import { userService } from '../../../src/services/user.service.js'; describe('user.service', () => { @@ -98,5 +115,105 @@ describe('user.service', () => { expect(result.data.user.email).toBeUndefined(); expect(result.data.user.orgs).toEqual([]); }); + + it('should return SSO user profile when org matches app organization', async () => { + mockChainValue.mockReturnValue(0); + mockRequestWithSsoTokenRefresh.mockResolvedValue([ + null, + { + status: 200, + data: { + user: { + email: 'sso@example.com', + first_name: 'S', + last_name: 'O', + organizations: [{ uid: 'org-1', name: 'Org 1' }], + }, + }, + }, + ]); + + const result = await userService.getUserProfile({ + body: { + token_payload: { region: 'NA', user_id: 'user-123', is_sso: true }, + }, + } as any); + + expect(result.status).toBe(200); + expect(result.data.user.email).toBe('sso@example.com'); + expect(result.data.user.orgs).toEqual([ + { org_id: 'org-1', org_name: 'Test Org' }, + ]); + }); + + it('should throw when SSO user has no access token', async () => { + mockChainValue.mockReturnValue(0); + const user = AuthenticationModel.data.users[0] as { + access_token?: string; + }; + const prev = user.access_token; + delete user.access_token; + + await expect( + userService.getUserProfile({ + body: { + token_payload: { region: 'NA', user_id: 'user-123', is_sso: true }, + }, + } as any) + ).rejects.toMatchObject({ message: 'SSO authentication not completed' }); + + user.access_token = prev; + }); + + it('should return error payload when SSO CS request fails', async () => { + mockChainValue.mockReturnValue(0); + mockRequestWithSsoTokenRefresh.mockResolvedValue([ + { response: { data: { error: 'bad' }, status: 403 } }, + null, + ]); + + const result = await userService.getUserProfile({ + body: { + token_payload: { region: 'NA', user_id: 'user-123', is_sso: true }, + }, + } as any); + + expect(result.status).toBe(403); + expect(result.data).toEqual({ error: 'bad' }); + }); + + it('should throw when SSO user org list does not include app org', async () => { + mockChainValue.mockReturnValue(0); + mockRequestWithSsoTokenRefresh.mockResolvedValue([ + null, + { + status: 200, + data: { + user: { + organizations: [{ uid: 'other-org', name: 'Other' }], + }, + }, + }, + ]); + + await expect( + userService.getUserProfile({ + body: { + token_payload: { region: 'NA', user_id: 'user-123', is_sso: true }, + }, + } as any) + ).rejects.toMatchObject({ message: 'Organization access revoked' }); + }); + + it('should wrap unexpected errors in ExceptionFunction', async () => { + mockChainValue.mockReturnValue(0); + vi.mocked(getAppOrganization).mockImplementationOnce(() => { + throw new Error('unexpected'); + }); + + await expect( + userService.getUserProfile(createReq() as any) + ).rejects.toMatchObject({ message: 'unexpected' }); + }); }); }); diff --git a/api/vitest.config.ts b/api/vitest.config.ts index 5b452adb4..f1512c4f7 100644 --- a/api/vitest.config.ts +++ b/api/vitest.config.ts @@ -35,10 +35,10 @@ export default defineConfig({ 'src/models/types.ts', ], thresholds: { - lines: 77, + lines: 76, functions: 80, - branches: 57, - statements: 77, + branches: 56, + statements: 76, }, }, }, diff --git a/app.json b/app.json index 5e60a8d97..9708dec6c 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-02-23T07:26:46.225Z", + "timestamp": "2026-04-08T12:40:32.171Z", "region": { "key": "NA", "name": "North America", @@ -14,21 +14,21 @@ } }, "user": { - "email": "user@example.com", - "uid": "user-uid" + "email": "user@contentstack.com", + "uid": "user_id" }, "organization": { - "name": "Organization Name", - "uid": "organization-uid" + "name": "organization_name", + "uid": "organization_uid" }, "app": { - "name": "Migration Tool", - "uid": "app-uid", - "manifest": "Migration Tool" + "name": "app_name", + "uid": "app_uid", + "manifest": "app_manifest" }, "oauthData": { - "client_id": "client-id", - "client_secret": "client-secret", + "client_id": "user_client_id", + "client_secret": "user_client_secret", "redirect_uri": "http://localhost:5001/v2/auth/save-token", "user_token_config": { "enabled": true, @@ -182,9 +182,9 @@ } }, "pkce": { - "code_verifier": "code-verifier", - "code_challenge": "code-challenge" + "code_verifier": "code_verifier", + "code_challenge": "code_challenge" }, - "authUrl": "auth-url", - "isDefault": true + "authUrl": "auth_url", + "isDefault": false } \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 8ff49b067..c2d1d164e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -37,14 +37,14 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@vitest/coverage-v8": "^4.0.18", - "@vitest/ui": "^4.0.18", + "@vitest/coverage-v8": "4.1.2", + "@vitest/ui": "4.1.2", "eslint": "^8.51.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^28.1.0", "prettier": "^3.3.3", - "vitest": "^4.0.18" + "vitest": "4.1.2" } }, "node_modules/@acemir/cssom": { @@ -2712,14 +2712,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.0.tgz", - "integrity": "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", + "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.2", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2727,14 +2727,14 @@ "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.0", - "vitest": "4.1.0" + "@vitest/browser": "4.1.2", + "vitest": "4.1.2" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2743,31 +2743,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2776,7 +2776,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2788,26 +2788,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -2815,14 +2815,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2831,9 +2831,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -2862,20 +2862,7 @@ "vitest": "4.1.2" } }, - "node_modules/@vitest/ui/node_modules/@vitest/pretty-format": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", - "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui/node_modules/@vitest/utils": { + "node_modules/@vitest/utils": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", @@ -2890,21 +2877,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.0", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -9205,19 +9177,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -9228,8 +9200,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -9245,13 +9217,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { diff --git a/ui/package.json b/ui/package.json index 177ca60a2..17367d847 100644 --- a/ui/package.json +++ b/ui/package.json @@ -47,14 +47,14 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", - "@vitest/coverage-v8": "^4.0.18", - "@vitest/ui": "^4.0.18", + "@vitest/coverage-v8": "4.1.2", + "@vitest/ui": "4.1.2", "eslint": "^8.51.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^28.1.0", "prettier": "^3.3.3", - "vitest": "^4.0.18" + "vitest": "4.1.2" }, "overrides": { "@babel/runtime": ">=7.26.10", diff --git a/ui/src/components/AdvancePropertise/index.tsx b/ui/src/components/AdvancePropertise/index.tsx index 181654807..d413f73df 100644 --- a/ui/src/components/AdvancePropertise/index.tsx +++ b/ui/src/components/AdvancePropertise/index.tsx @@ -40,6 +40,26 @@ interface Taxonomy { source?: string; } +/** From `advanced.taxonomies` + saved uids → rows with uid + label; deduped by uid. */ +function taxonomyRows( + oldList: Array<{ taxonomy_uid?: string; taxonomy_name?: string; name?: string } | string>, + extraUids: string[] +): { taxonomy_uid: string; taxonomy_name: string }[] { + const m = new Map(); + for (const t of oldList) { + const id = typeof t === 'string' ? t : t.taxonomy_uid || ''; + if (!id) continue; + m?.set(id, { + taxonomy_uid: id, + taxonomy_name: typeof t === 'string' ? t : t?.taxonomy_name || t?.name || id + }); + } + for (const id of extraUids) { + if (id && !m?.has(id)) m?.set(id, { taxonomy_uid: id, taxonomy_name: id }); + } + return [...m.values()]; +} + /** * Component for displaying advanced properties. * @param props - The schema properties. @@ -158,22 +178,9 @@ const AdvancePropertise = (props: SchemaProps) => { if (props?.fieldtype === 'Taxonomy') { fetchTaxonomies(); - // Initialize referencedTaxonomies from existing data if available - const oldTaxonomies = props?.data?.advanced?.taxonomies || []; - const newTaxonomies = getMappedTaxonomyUids(); - const allTaxonomyUIDs = Array.from( - new Set([ - ...oldTaxonomies.map((t: { taxonomy_uid?: string } | string) => - typeof t === 'string' ? t : t.taxonomy_uid || '' - ), - ...newTaxonomies, - ]) - ).filter(Boolean); - - if (allTaxonomyUIDs.length > 0) { - setReferencedTaxonomies( - allTaxonomyUIDs.map((uid: string) => ({ label: uid, value: uid })) - ); + const rows = taxonomyRows(props?.data?.advanced?.taxonomies || [], getMappedTaxonomyUids()); + if (rows?.length > 0) { + setReferencedTaxonomies(rows?.map((r) => ({ label: r?.taxonomy_name, value: r?.taxonomy_uid }))); } } }, [props?.projectId, props?.fieldtype]); @@ -209,37 +216,24 @@ const AdvancePropertise = (props: SchemaProps) => { // Only proceed if we have taxonomies loaded OR if we have existing taxonomy data to match if (allTaxonomies.length > 0 || props?.data?.advanced?.taxonomies || getMappedTaxonomyUids().length > 0) { - // Merge old (upload-api) and new (UI) selections - const oldTaxonomies = (props?.data?.advanced?.taxonomies || []).map((t: { taxonomy_uid?: string } | string) => (typeof t === 'string' ? t : t.taxonomy_uid || '')); - const newTaxonomies = getMappedTaxonomyUids(); - const allTaxonomyUIDs = Array.from(new Set([...oldTaxonomies, ...newTaxonomies])); - - if (allTaxonomyUIDs.length > 0 && allTaxonomies.length > 0) { - // Match UIDs with loaded taxonomies + const rows = taxonomyRows(props?.data?.advanced?.taxonomies || [], getMappedTaxonomyUids()); + const allTaxonomyUIDs = rows?.map((r) => r?.taxonomy_uid); + + if (allTaxonomyUIDs?.length > 0 && allTaxonomies?.length > 0) { const matchedTaxonomies = allTaxonomyUIDs .map((uid: string) => { - const taxonomy = allTaxonomies.find((t: Taxonomy) => t.uid === uid); - return taxonomy ? { label: taxonomy.name || taxonomy.uid, value: taxonomy.uid } : null; + const taxonomy = allTaxonomies?.find((t: Taxonomy) => t?.uid === uid); + return taxonomy ? { label: taxonomy.name || taxonomy?.uid, value: taxonomy?.uid } : null; }) .filter(Boolean) as ContentTypeOption[]; - - if (matchedTaxonomies.length > 0) { + + if (matchedTaxonomies?.length > 0) { setReferencedTaxonomies(matchedTaxonomies); } else { - // If no matches found but we have UIDs, create options from UIDs (fallback) - const fallbackOptions = allTaxonomyUIDs.map((uid: string) => ({ - label: uid, - value: uid - })); - setReferencedTaxonomies(fallbackOptions); + setReferencedTaxonomies(rows?.map((r) => ({ label: r?.taxonomy_name, value: r?.taxonomy_uid }))); } - } else if (allTaxonomyUIDs.length > 0 && allTaxonomies.length === 0) { - // Taxonomies not loaded yet, but we have UIDs - create fallback options - const fallbackOptions = allTaxonomyUIDs.map((uid: string) => ({ - label: uid, - value: uid - })); - setReferencedTaxonomies(fallbackOptions); + } else if (allTaxonomyUIDs?.length > 0 && allTaxonomies?.length === 0) { + setReferencedTaxonomies(rows?.map((r) => ({ label: r?.taxonomy_name, value: r?.taxonomy_uid }))); } else { // No existing taxonomies, clear the selection setReferencedTaxonomies(null); diff --git a/ui/src/components/ContentMapper/__tests__/groupSchema.utils.test.ts b/ui/src/components/ContentMapper/__tests__/groupSchema.utils.test.ts index 53fdf1dc4..ef6acad6a 100644 --- a/ui/src/components/ContentMapper/__tests__/groupSchema.utils.test.ts +++ b/ui/src/components/ContentMapper/__tests__/groupSchema.utils.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'; import { shouldAddGroupOption, shouldRecurseIntoNestedDestGroup, + findGroupFieldInChildren, } from '../groupSchema.utils'; import type { FieldMapType, ExistingFieldType, ContentTypesSchema } from '../contentMapper.interface'; @@ -333,3 +334,46 @@ describe('shouldRecurseIntoNestedDestGroup', () => { }); }); }); + +// --------------------------------------------------------------------------- +// findGroupFieldInChildren +// --------------------------------------------------------------------------- + +describe('findGroupFieldInChildren', () => { + it('returns a group at the first level of children', () => { + const inner: FieldMapType = { + ...makeField('mb.quote.details', 'd_b'), + contentstackFieldType: 'group', + }; + const children: FieldMapType[] = [inner]; + expect(findGroupFieldInChildren(children, 'mb.quote.details')).toBe(inner); + }); + + it('finds a nested group when it is not a direct child', () => { + const details: FieldMapType = { + ...makeField('mb.quote.details', 'd_b'), + contentstackFieldType: 'group', + child: [], + }; + const quoteWrap: FieldMapType = { + ...makeField('mb.quote', 'q_b'), + contentstackFieldType: 'group', + child: [details], + }; + const children = [quoteWrap]; + expect(findGroupFieldInChildren(children, 'mb.quote.details')).toBe(details); + }); + + it('returns undefined when uid does not exist', () => { + expect(findGroupFieldInChildren([], 'x')).toBeUndefined(); + expect(findGroupFieldInChildren(undefined, 'x')).toBeUndefined(); + }); + + it('does not match non-group fields with the same uid', () => { + const leaf: FieldMapType = { + ...makeField('mb.quote.paragraph', 'p_b'), + contentstackFieldType: 'json', + }; + expect(findGroupFieldInChildren([leaf], 'mb.quote.paragraph')).toBeUndefined(); + }); +}); diff --git a/ui/src/components/ContentMapper/groupSchema.utils.ts b/ui/src/components/ContentMapper/groupSchema.utils.ts index fdd3dfd4f..1f34e5ffb 100644 --- a/ui/src/components/ContentMapper/groupSchema.utils.ts +++ b/ui/src/components/ContentMapper/groupSchema.utils.ts @@ -1,5 +1,25 @@ import { FieldMapType, ExistingFieldType } from './contentMapper.interface'; +/** + * Finds a source group field anywhere under a modular block child's `child` tree + * by uid. Direct `.find` on immediate children fails when groups are nested + * (e.g. quote → details → paragraph). + */ +export function findGroupFieldInChildren( + children: FieldMapType[] | undefined, + uid: string, +): FieldMapType | undefined { + if (!children?.length || !uid) return undefined; + for (const c of children) { + if (c?.uid === uid && c?.contentstackFieldType === 'group') { + return c; + } + const found = findGroupFieldInChildren(c?.child, uid); + if (found) return found; + } + return undefined; +} + /** * Determines whether a destination group field should be offered as a mapping * option for a source group field, by enforcing equal nesting depths. @@ -44,7 +64,9 @@ export function shouldRecurseIntoNestedDestGroup( } const parentSourceUid = sourceUidParts?.slice(0, -1)?.join('.'); + const parentSourceNode = nestedList?.find((item: FieldMapType) => item?.uid === parentSourceUid); + const parentMappedLabel = parentSourceNode?.backupFieldUid ? (existingField[parentSourceNode.backupFieldUid] as { label?: string })?.label : undefined; diff --git a/ui/src/components/ContentMapper/index.scss b/ui/src/components/ContentMapper/index.scss index ce3df6c62..09a207a19 100644 --- a/ui/src/components/ContentMapper/index.scss +++ b/ui/src/components/ContentMapper/index.scss @@ -483,4 +483,10 @@ div .table-row { .mapper-footer { padding: 10px; } +} + +.select { + .tippy-wrapper { + display: flex; + } } \ No newline at end of file diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 0798e395f..99d73fb11 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -1,5 +1,12 @@ // Libraries -import { useEffect, useState, useRef, useImperativeHandle, forwardRef } from 'react'; +import { + useEffect, + useState, + useRef, + useImperativeHandle, + forwardRef, + type ComponentProps, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import { @@ -68,12 +75,24 @@ import AdvanceSettings from '../AdvancePropertise'; import SaveChangesModal from '../Common/SaveChangesModal'; // Utilities -import { shouldAddGroupOption, shouldRecurseIntoNestedDestGroup } from './groupSchema.utils'; +import { + shouldAddGroupOption, + shouldRecurseIntoNestedDestGroup, + findGroupFieldInChildren, +} from './groupSchema.utils'; // Styles and Assets import './index.scss'; import { NoDataFound, SCHEMA_PREVIEW } from '../../common/assets'; +/** Renders the menu in the document body so `menuPlacement="auto"` matches the control when inside scroll/overflow containers (e.g. InfiniteScrollTable). */ +const CONTENT_MAPPER_SELECT_MENU_PORTAL = + typeof document !== 'undefined' ? document.body : undefined; + +const contentMapperSelectMenuStyles = { + menuPortal: (base: Record) => ({ ...base, zIndex: 10001 }), +}; + const rowHistoryObj: FieldHistoryObj = {} const Fields: MappingFields = { @@ -275,6 +294,74 @@ const flattenSchemaToUidMap = ( return result; }; +/** Match saved `contentstackField` labels against a modular-blocks subtree (block + fields + nested). */ +const matchRowAgainstModularBlocks = ( + row: FieldMapType, + mbFieldPath: string, + blocks: ContentTypesSchema[], + isFieldDeleted: boolean, + addMatch: (backupFieldUid: string, label: string, value: ContentTypesSchema) => void +) => { + if (!blocks?.length) return; + + for (const block of blocks) { + const blockTitle = block?.uid || block?.display_name; + const blockDisplayName = `${mbFieldPath} > ${blockTitle}`; + + if (row?.contentstackField === blockDisplayName && !isFieldDeleted) { + addMatch(row?.backupFieldUid, blockDisplayName, block as unknown as ContentTypesSchema); + } + + if (block?.schema) { + for (const blockField of block.schema) { + const fieldDisplayName = `${blockDisplayName} > ${blockField?.display_name}`; + + if (row?.contentstackField === fieldDisplayName && !isFieldDeleted) { + addMatch(row?.backupFieldUid, fieldDisplayName, blockField); + } + + if (blockField?.schema) { + for (const nestedField of blockField.schema) { + const nestedDisplayName = `${fieldDisplayName} > ${nestedField?.display_name}`; + if (row?.contentstackField === nestedDisplayName && !isFieldDeleted) { + addMatch(row?.backupFieldUid, nestedDisplayName, nestedField); + } + } + } + + if (blockField?.data_type === 'blocks' && blockField?.blocks) { + for (const nestedBlock of blockField.blocks as ContentTypesSchema[]) { + const nestedBlockTitle = nestedBlock?.uid || nestedBlock?.display_name; + const nestedBlockDisplayName = `${fieldDisplayName} > ${nestedBlockTitle}`; + + if (row?.contentstackField === nestedBlockDisplayName && !isFieldDeleted) { + addMatch(row?.backupFieldUid, nestedBlockDisplayName, nestedBlock as ContentTypesSchema); + } + + if (nestedBlock?.schema) { + for (const nestedBlockField of nestedBlock.schema) { + const nestedFieldDisplayName = `${nestedBlockDisplayName} > ${nestedBlockField?.display_name}`; + if (row?.contentstackField === nestedFieldDisplayName && !isFieldDeleted) { + addMatch(row?.backupFieldUid, nestedFieldDisplayName, nestedBlockField); + } + + if (nestedBlockField?.schema) { + for (const deepField of nestedBlockField.schema) { + const deepDisplayName = `${nestedFieldDisplayName} > ${deepField?.display_name}`; + if (row?.contentstackField === deepDisplayName && !isFieldDeleted) { + addMatch(row?.backupFieldUid, deepDisplayName, deepField); + } + } + } + } + } + } + } + } + } + } +}; + const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: React.ForwardedRef) => { /** ALL CONTEXT HERE */ @@ -358,6 +445,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: const filterRef = useRef(null); const tableWrapperRef = useRef(null); const clearedFieldsRef = useRef>(new Set()); + const prevOtherContentTypeIdRef = useRef(undefined); /********** ALL USEEFFECT HERE *************/ useEffect(() => { @@ -435,195 +523,109 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: // useEffect for rendering mapped fields with existing stack useEffect(() => { + if (!contentTypeSchema || contentTypeSchema.length === 0) return; + if (newMigrationData?.content_mapping?.content_type_mapping?.[selectedContentType?.contentstackUid || ''] !== otherContentType?.id) return; + const nextExistingField: ExistingFieldType = { ...existingField }; + const nextSelectedOptions: string[] = [...selectedOptions]; + let anyMatch = false; - if (newMigrationData?.content_mapping?.content_type_mapping?.[selectedContentType?.contentstackUid || ''] === otherContentType?.id) { - setIsAllCheck(false); - - tableData?.forEach((row) => { - contentTypeSchema?.forEach((schema) => { - - if (row?.contentstackField === schema?.display_name) { - if (!updatedSelectedOptions?.includes?.(schema?.display_name)) { - updatedSelectedOptions.push(schema?.display_name); - } - updatedExstingField[row?.backupFieldUid] = { - label: schema?.display_name, - value: schema - }; - } - - // 1st level group nesting - if (schema?.schema) { - schema?.schema?.forEach((childSchema) => { - if (row?.contentstackField === `${schema?.display_name} > ${childSchema?.display_name}`) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(`${schema?.display_name} > ${childSchema?.display_name}`)) { - updatedSelectedOptions.push(`${schema?.display_name} > ${childSchema?.display_name}`); - } - updatedExstingField[row?.backupFieldUid] = { - label: `${schema?.display_name} > ${childSchema?.display_name}`, - value: childSchema - } - } - } + const addMatch = (backupFieldUid: string, label: string, value: ContentTypesSchema) => { + if (!nextSelectedOptions.includes(label)) { + nextSelectedOptions.push(label); + } + nextExistingField[backupFieldUid] = { label, value }; + anyMatch = true; + }; - // 2nd level group nesting - if (childSchema?.schema) { - childSchema?.schema?.forEach((nestedSchema) => { - if (row?.contentstackField === `${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name}`) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(`${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name}`)) { - updatedSelectedOptions.push(`${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name}`); - } - updatedExstingField[row?.backupFieldUid] = { - label: `${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name}`, - value: nestedSchema - } - } - } + setIsAllCheck(false); - // 3rd level group nesting - if (nestedSchema?.schema) { - nestedSchema?.schema?.forEach((nestedChild) => { - if (row?.contentstackField === `${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name} > ${nestedChild?.display_name}`) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(`${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name} > ${nestedChild?.display_name}`)) { - updatedSelectedOptions.push(`${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name} > ${nestedChild?.display_name}`); - } - updatedExstingField[row?.backupFieldUid] = { - label: `${schema?.display_name} > ${childSchema?.display_name} > ${nestedSchema?.display_name} > ${nestedChild?.display_name}`, - value: nestedChild - } - } - } - }) - } - }) - } + tableData?.forEach((row) => { + if (!row?.contentstackField || row?.contentstackField === row?.otherCmsField) return; - // Modular blocks mapping - if (schema?.data_type === 'blocks' && schema?.blocks) { - schema?.blocks?.forEach((block) => { - const blockTitle = block?.uid || block?.display_name; - const blockDisplayName = `${schema?.display_name} > ${blockTitle}`; - - // Modular block child - if (row?.contentstackField === blockDisplayName) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(blockDisplayName)) { - updatedSelectedOptions.push(blockDisplayName); - } - updatedExstingField[row?.backupFieldUid] = { - label: blockDisplayName, - value: block - }; - } - } + contentTypeSchema?.forEach((schema) => { + if (row?.contentstackField === schema?.display_name) { + addMatch(row?.backupFieldUid, schema?.display_name, schema); + } - // Fields within modular block child - if (block?.schema) { - block?.schema?.forEach((blockField) => { - const fieldDisplayName = `${blockDisplayName} > ${blockField?.display_name}`; - - if (row?.contentstackField === fieldDisplayName) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(fieldDisplayName)) { - updatedSelectedOptions?.push(fieldDisplayName); - } - updatedExstingField[row?.backupFieldUid] = { - label: fieldDisplayName, - value: blockField - }; - } - } + // Root-level modular blocks: Contentstack uses `blocks`, not `schema` — must not be nested under group-only handling + if (schema?.data_type === 'blocks' && schema?.blocks) { + matchRowAgainstModularBlocks( + row, + schema?.display_name ?? '', + schema.blocks as ContentTypesSchema[], + isFieldDeleted, + addMatch + ); + } - // Nested group within modular block child field - if (blockField?.schema) { - blockField?.schema?.forEach((nestedField) => { - const nestedDisplayName = `${fieldDisplayName} > ${nestedField?.display_name}`; + // 1st level group nesting + if (schema?.schema) { + schema?.schema?.forEach((childSchema) => { + const label1 = `${schema?.display_name} > ${childSchema?.display_name}`; + if (row?.contentstackField === label1 && !isFieldDeleted) { + addMatch(row?.backupFieldUid, label1, childSchema); + } - if (row?.contentstackField === nestedDisplayName) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(nestedDisplayName)) { - updatedSelectedOptions?.push(nestedDisplayName); - } - updatedExstingField[row?.backupFieldUid] = { - label: nestedDisplayName, - value: nestedField - }; - } - } - }); - } + // Modular blocks field nested inside a group + if (childSchema?.data_type === 'blocks' && childSchema?.blocks) { + matchRowAgainstModularBlocks( + row, + label1, + childSchema.blocks as ContentTypesSchema[], + isFieldDeleted, + addMatch + ); + } - // Nested modular blocks within child block field - if (blockField?.data_type === 'blocks' && blockField?.blocks) { - blockField?.blocks?.forEach((nestedBlock: any) => { - const nestedBlockTitle = nestedBlock?.uid || nestedBlock?.display_name; - const nestedBlockDisplayName = `${fieldDisplayName} > ${nestedBlockTitle}`; + // 2nd level group nesting + if (childSchema?.schema) { + childSchema?.schema?.forEach((nestedSchema) => { + const label2 = `${label1} > ${nestedSchema?.display_name}`; + if (row?.contentstackField === label2 && !isFieldDeleted) { + addMatch(row?.backupFieldUid, label2, nestedSchema); + } - if (row?.contentstackField === nestedBlockDisplayName) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(nestedBlockDisplayName)) { - updatedSelectedOptions?.push(nestedBlockDisplayName); - } - updatedExstingField[row?.backupFieldUid] = { - label: nestedBlockDisplayName, - value: nestedBlock - }; - } - } - - if (nestedBlock?.schema) { - nestedBlock?.schema?.forEach((nestedBlockField: any) => { - const nestedFieldDisplayName = `${nestedBlockDisplayName} > ${nestedBlockField?.display_name}`; - - if (row?.contentstackField === nestedFieldDisplayName) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(nestedFieldDisplayName)) { - updatedSelectedOptions?.push(nestedFieldDisplayName); - } - updatedExstingField[row?.backupFieldUid] = { - label: nestedFieldDisplayName, - value: nestedBlockField - }; - } - } + // 3rd level group nesting + if (nestedSchema?.schema) { + nestedSchema?.schema?.forEach((nestedChild) => { + const label3 = `${label2} > ${nestedChild?.display_name}`; + if (row?.contentstackField === label3 && !isFieldDeleted) { + addMatch(row?.backupFieldUid, label3, nestedChild); + } - if (nestedBlockField?.schema) { - nestedBlockField?.schema?.forEach((deepField: any) => { - const deepDisplayName = `${nestedFieldDisplayName} > ${deepField?.display_name}`; - - if (row?.contentstackField === deepDisplayName) { - if (!isFieldDeleted) { - if (!updatedSelectedOptions?.includes?.(deepDisplayName)) { - updatedSelectedOptions?.push(deepDisplayName); - } - updatedExstingField[row?.backupFieldUid] = { - label: deepDisplayName, - value: deepField - }; - } - } - }); - } - }); - } - }); - } - }); - } - }); - } - }); - } - }); + // Deeper nesting: modular blocks inside 3rd-level group field + if (nestedChild?.data_type === 'blocks' && nestedChild?.blocks) { + const mbPath = `${label3}`; + matchRowAgainstModularBlocks( + row, + mbPath, + nestedChild.blocks as ContentTypesSchema[], + isFieldDeleted, + addMatch + ); + } + }); + } + }); + } + }); + } }); - setSelectedOptions(updatedSelectedOptions); - setExistingField(updatedExstingField); + }); + + if (anyMatch) { + setSelectedOptions(nextSelectedOptions); + setExistingField(nextExistingField); } - }, [tableData, otherContentType]); + }, [ + tableData, + otherContentType?.id, + contentTypeSchema, + newMigrationData, + selectedContentType?.contentstackUid, + isFieldDeleted, + ]); useEffect(() => { if (isUpdated) { @@ -636,12 +638,15 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: } setIsUpdated(false); } - else { + else if ( + prevOtherContentTypeIdRef.current !== undefined && + prevOtherContentTypeIdRef.current !== otherContentType?.id + ) { setIsAllCheck(false); setExistingField({}); setSelectedOptions([]); - } + prevOtherContentTypeIdRef.current = otherContentType?.id; }, [isUpdated, otherContentType]); // To make all the fields checked @@ -1515,6 +1520,8 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: isClearable={false} options={option} menuPlacement="auto" + menuPortalTarget={CONTENT_MAPPER_SELECT_MENU_PORTAL} + styles={contentMapperSelectMenuStyles} isDisabled={ !(data?.contentstackFieldType === 'single_line_text' || data?.contentstackFieldType === 'multi_line_text' || data?.contentstackFieldType === 'html' || data?.contentstackFieldType === 'json') || @@ -1898,8 +1905,10 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: const blockUid = `${uid}.${block?.uid}`; const parentBlockUid = data?.uid?.split('.')?.slice(0, -1)?.join('.'); + const parentBlockItem = tableData?.find(item => item?.uid === parentBlockUid); + const parentBlockKey = parentBlockItem?.backupFieldUid ?? parentBlockUid; - if (data?.backupFieldType === 'modular_blocks_child' && existingField[parentBlockUid]?.label === updatedDisplayName) { + if (data?.backupFieldType === 'modular_blocks_child' && existingField[parentBlockKey]?.label === updatedDisplayName) { const blockOption: ContentTypesSchema = { ...block, data_type: block?.data_type || undefined, @@ -1987,16 +1996,15 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: } // Only process fields if current block matches the mapped block - if (mappedChildBlockTitle === blockTitle) { + if (mappedChildBlockTitle === blockTitle && existingChildBlockMapping?.label === blockDisplayName) { const parentDepth = dataParentChildBlockUid?.split('.')?.length ?? 0; const isDataInsideGroupField = (data?.uid?.split('.')?.length ?? 0) > parentDepth + 1; - for (const blockField of block?.schema ?? []) { const fieldTypeToMatch = Fields[data?.backupFieldType as keyof Mapping]?.type; if (!isDataInsideGroupField && checkConditions(fieldTypeToMatch, blockField, data) && blockField?.data_type !== 'group' && blockField?.data_type !== 'blocks') { const fieldDisplayName = `${blockDisplayName} > ${blockField?.display_name}`; const fieldUid = `${blockUid}.${blockField?.uid}`; - + OptionsForRow.push(getMatchingOption( blockField, true, @@ -2021,8 +2029,17 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: // Recursively process nested groups within block fields — group options are added inside processSchema's group handler if (blockField?.data_type === 'group' && blockField?.schema) { + const dataChildBlockUid = dataParentChildBlockUid; - const dataGroupUid = data?.uid?.split('.')?.slice(0, parentDepth + 1)?.join('.'); + // Parent source group uid for nestedList lookup: + // - Group rows map the group itself (full data.uid). + // - Leaf fields (e.g. paragraph under details) must use the immediate + // parent uid. slice(0, parentDepth + 1) breaks when multiple groups + // sit between the child block and the leaf (quote → details → paragraph). + const dataGroupUid = + data?.backupFieldType === 'group' && data?.contentstackFieldType === 'group' + ? data?.uid ?? '' + : data?.uid?.split('.')?.slice(0, -1)?.join('.') ?? ''; const modularBlock = nestedList?.find(item => item?.contentstackFieldType === 'modular_blocks' && @@ -2031,13 +2048,12 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: const childBlock = modularBlock?.child?.find( (c: FieldMapType) => c?.uid === dataChildBlockUid ); - const groupField = childBlock?.child?.find( - (c: FieldMapType) => c?.uid === dataGroupUid && c?.contentstackFieldType === 'group' - ); + const groupField = findGroupFieldInChildren(childBlock?.child, dataGroupUid); const groupChildren = groupField?.child || []; const groupArr = groupField ? [groupField] : []; - + + processSchema( blockField, data, @@ -2074,7 +2090,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: return OptionsForRow; } else if (value?.data_type === 'group') { - + if (data?.backupFieldType === 'group' && checkConditions('Group', value, data) ) { if (shouldAddGroupOption(data?.uid ?? '', parentUid)) { const newOption = getMatchingOption(value, true, updatedDisplayName, uid ?? ''); @@ -2086,7 +2102,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: } } } - + const existingLabel = existingField[groupArray?.[0]?.backupFieldUid]?.label ?? ''; const lastLabelSegment = existingLabel?.includes('>') @@ -2094,6 +2110,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: : existingLabel; if (value?.display_name === lastLabelSegment) { + const groupUid = groupArray?.[0]?.uid ?? ''; const groupDepth = groupUid?.split('.')?.length ?? 0; @@ -2101,7 +2118,6 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: const fieldTypeToMatch = Fields[item?.backupFieldType as keyof Mapping]?.type; const itemDepth = item?.uid?.split('.')?.length ?? 0; const isRootLevelChild = itemDepth === groupDepth + 1; - if (item?.id === data?.id && isRootLevelChild) { for (const key of existingField[groupArray?.[0]?.backupFieldUid]?.value?.schema || []) { if (checkConditions(fieldTypeToMatch, key, item)) { @@ -2118,7 +2134,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: for (const key of existingField[groupArray?.[0]?.backupFieldUid]?.value?.schema || []) { if (key?.data_type === 'group') { - + const nestedGroupUid = data?.uid?.split('.')?.slice(0, groupDepth + 1)?.join('.'); const nestedGroupField = groupArray?.[0]?.child?.find( @@ -2133,6 +2149,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: } else { + if (shouldRecurseIntoNestedDestGroup(data?.uid ?? '', updatedDisplayName, nestedList ?? [], existingField)) { for (const key of value?.schema || []) { if (key?.data_type === 'group') { @@ -2169,6 +2186,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: // Recursively process nested groups if (key?.data_type === 'group') { + processSchema(key, data, array, groupArray, OptionsForRow, fieldsOfContentstack, updatedDisplayName, uid); } } @@ -2392,28 +2410,41 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: const isTypeMatch = checkConditions(Fields[data?.contentstackFieldType]?.type, existingField[data?.backupFieldUid]?.value, data); + const selectValueIsExistingField = + OptionsForRow?.length !== 0 && + isTypeMatch && + existingField?.[data?.backupFieldUid]?.label !== undefined; + return (
- { + if (OptionsForRow?.length === 0) { + handleValueChange(selectedOption, data?.uid, data?.backupFieldUid) + } else { + handleFieldChange(selectedOption, data?.uid, data?.contentstackFieldUid, data?.backupFieldUid) + } + }} + placeholder="Select Field" + version={'v2'} + maxWidth="290px" + isClearable={isTypeMatch && selectedOptions?.includes?.(existingField?.[data?.backupFieldUid]?.label ?? '')} + options={adjustedOptions} + isDisabled={OptionValue?.isDisabled || newMigrationData?.project_current_step > 4} + menuPlacement="auto" + menuPortalTarget={CONTENT_MAPPER_SELECT_MENU_PORTAL} + styles={contentMapperSelectMenuStyles} + /> +
{(!OptionValue?.isDisabled || OptionValue?.label === 'Dropdown' || (data?.backupFieldType !== 'extension' && diff --git a/upload-api/migration-aem/package-lock.json b/upload-api/migration-aem/package-lock.json index 28c9def83..79a596151 100644 --- a/upload-api/migration-aem/package-lock.json +++ b/upload-api/migration-aem/package-lock.json @@ -108,6 +108,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -755,6 +766,14 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/upload-api/migration-contentful/index.js b/upload-api/migration-contentful/index.js index 045af75ec..d38b50625 100644 --- a/upload-api/migration-contentful/index.js +++ b/upload-api/migration-contentful/index.js @@ -3,9 +3,11 @@ const extractContentTypes = require('./libs/extractContentTypes'); const createInitialMapper = require('./libs/createInitialMapper'); const extractLocale = require('./libs/extractLocale'); +const extractTaxonomy = require('./libs/extractTaxonomy'); module.exports = { extractContentTypes, createInitialMapper, - extractLocale + extractLocale, + extractTaxonomy }; diff --git a/upload-api/migration-contentful/libs/createInitialMapper.js b/upload-api/migration-contentful/libs/createInitialMapper.js index 331359311..3a3cdc761 100644 --- a/upload-api/migration-contentful/libs/createInitialMapper.js +++ b/upload-api/migration-contentful/libs/createInitialMapper.js @@ -9,6 +9,54 @@ const path = require('path'); // const contentTypeMapper = require('./contentTypeMapper'); const contentTypeMapper = require('./contentTypeMapper'); +/** Contentstack taxonomy_uid: lowercase, a-z0-9_ only */ +function contentfulSchemeIdToStackTaxonomyUid(contentfulSchemeId) { + if (!contentfulSchemeId || typeof contentfulSchemeId !== 'string') return ''; + return contentfulSchemeId + .replace(/([A-Z])/g, '_$1') + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + +/** + * Maps Contentful content-type metadata.taxonomy (TaxonomyConceptScheme links) to a Contentstack taxonomy field. + * Field uid must be `taxonomies` Taxonomy fields must be localizable. + * @param {object|undefined} metadata - Content type `metadata` from export JSON. + * @returns {object[]} Field mapping rows (empty if no taxonomy). + */ +const buildContentfulTaxonomyFields = (metadata) => { + const links = metadata?.taxonomy; + if (!Array.isArray(links) || !links.length) return []; + const schemes = links + .map((t) => contentfulSchemeIdToStackTaxonomyUid(t?.sys?.id)) + .filter(Boolean); + if (!schemes.length) return []; + return [ + { + uid: 'taxonomies', + otherCmsField: 'Contentful taxonomy (metadata)', + otherCmsType: 'TaxonomyMetadata', + contentstackField: 'Taxonomies', + contentstackFieldUid: 'taxonomies', + contentstackFieldType: 'taxonomy', + backupFieldType: 'taxonomy', + backupFieldUid: 'taxonomies', + advanced: { + taxonomies: schemes.map((schemeUid) => ({ + taxonomy_uid: schemeUid, + mandatory: false, + multiple: true, + non_localizable: false + })), + mandatory: false, + multiple: true, + nonLocalizable: false + } + } + ]; +}; /** * Internal module dependencies. @@ -65,7 +113,14 @@ const uidCorrector = (uid, prefix) => { const createInitialMapper = async (cleanLocalPath, affix) => { try { const alldata = readFile(cleanLocalPath); - const { entries } = alldata; + const { entries, contentTypes: exportContentTypes = [] } = alldata; + + const ctMetaById = {}; + for (const ct of exportContentTypes) { + if (ct?.sys?.id) { + ctMetaById[ct.sys.id] = ct.metadata || {}; + } + } const initialMapper = []; const files = await fs.readdir( @@ -113,7 +168,10 @@ const createInitialMapper = async (cleanLocalPath, affix) => { advanced: { mandatory: true } } ]; - const contentstackFields = [...uidTitle, ...contentTypeMapper(data, entries)]?.filter?.( + const ctId = data?.[0]?.contentfulID; + const ctMetadata = ctMetaById[ctId] || {}; + const taxonomyRows = buildContentfulTaxonomyFields(ctMetadata); + const contentstackFields = [...uidTitle, ...contentTypeMapper(data, entries), ...taxonomyRows]?.filter?.( Boolean ); diff --git a/upload-api/migration-contentful/libs/extractTaxonomy.js b/upload-api/migration-contentful/libs/extractTaxonomy.js new file mode 100644 index 000000000..91f222a0d --- /dev/null +++ b/upload-api/migration-contentful/libs/extractTaxonomy.js @@ -0,0 +1,59 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const fs = require('fs'); +const path = require('path'); + +function contentfulSchemeIdToStackTaxonomyUid(contentfulSchemeId) { + if (!contentfulSchemeId || typeof contentfulSchemeId !== 'string') return ''; + return contentfulSchemeId + .replace(/([A-Z])/g, '_$1') + .toLowerCase() + .replace(/[^a-z0-9_]/g, '_') + .replace(/_+/g, '_') + .replace(/^_|_$/g, ''); +} + +/** Display name for mapper UI (product_category -> Product Category). */ +function humanizeSchemeId(id) { + if (!id || typeof id !== 'string') return ''; + return id + .split('_') + .filter(Boolean) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) + .join(' '); +} + +/** + * Collects unique TaxonomyConceptScheme ids from Contentful export content types (`metadata.taxonomy`). + * + * @param {string} filePath - Absolute path to the Contentful export JSON. + * @returns {Promise>} + */ +const extractTaxonomy = async (filePath) => { + const raw = await fs.promises.readFile(filePath, 'utf8'); + const data = JSON.parse(raw); + const contentTypes = data?.contentTypes || []; + const schemeIds = new Set(); + + for (const ct of contentTypes) { + const links = ct?.metadata?.taxonomy; + if (!Array.isArray(links)) continue; + for (const link of links) { + const sid = link?.sys?.id; + if (sid) schemeIds.add(contentfulSchemeIdToStackTaxonomyUid(sid)); + } + } + + const taxonomySchema = [...schemeIds].sort().map((uid) => ({ + uid, + name: humanizeSchemeId(uid) || uid, + })); + + const outputDir = path.join(process.cwd(), 'contentfulMigrationData', 'taxonomySchema'); + await fs.promises.mkdir(outputDir, { recursive: true }); + const outputPath = path.join(outputDir, 'taxonomySchema.json'); + await fs.promises.writeFile(outputPath, JSON.stringify(taxonomySchema, null, 2)); + + return taxonomySchema; +}; + +module.exports = extractTaxonomy; diff --git a/upload-api/migration-wordpress/libs/extractItems.ts b/upload-api/migration-wordpress/libs/extractItems.ts index da0948bfc..3b31c8aeb 100644 --- a/upload-api/migration-wordpress/libs/extractItems.ts +++ b/upload-api/migration-wordpress/libs/extractItems.ts @@ -5,7 +5,7 @@ import * as cheerio from 'cheerio'; import { setupWordPressBlocks } from "../utils/parseUtil"; -import { getFieldName, getFieldUid, schemaMapper } from "./schemaMapper"; +import { clientIdForUid, getFieldName, getFieldUid, schemaMapper } from "./schemaMapper"; import helper from "../utils/helper"; import config from '../config/index.json'; import extractTaxonomy from './extractTaxonomy'; @@ -25,6 +25,15 @@ function resolveBlockName(field: any): string { const { contentTypes: contentTypesConfig } = config?.modules; const contentTypeFolderPath = path.resolve(config?.data, contentTypesConfig?.dirName); +const blocksJsonOutputDir = path.resolve(config?.data, 'wordpress_blocks'); + +function sanitizeBlocksJsonFileName(title: string, maxLen = 80): string { + return String(title || 'untitled') + .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') + .trim() + .replace(/\s+/g, '_') + .slice(0, maxLen); +} function findSimilarBlocks(data: any[][], targetId: string) { for (const group of data) { @@ -276,7 +285,8 @@ const extractItems = async (item: any, config: DataConfig, type: string, affix: }, }; - for (const data of item) { + for (let itemIndex = 0; itemIndex < item?.length; itemIndex++) { + const data = item[itemIndex]; const processedSimilarBlocks = new Set(); const targetItem = items?.filter((i, el) => { return $(el)?.find("title")?.text() === data?.title; @@ -306,7 +316,7 @@ const extractItems = async (item: any, config: DataConfig, type: string, affix: const contentEncoded = targetItem?.find("content\\:encoded")?.text() || ''; const blocksJson = await setupWordPressBlocks(contentEncoded); - + // Example usage @@ -317,7 +327,7 @@ const extractItems = async (item: any, config: DataConfig, type: string, affix: // Track processed similar blocks to avoid duplicates for (const field of blocksJson) { - const fieldUid = getFieldUid(`${field?.name}_${field?.clientId}`|| '', affix || ''); + const fieldUid = getFieldUid(`${field?.name}_${clientIdForUid(field?.clientId)}`|| '', affix || ''); const contentstackFieldName = getFieldName(resolveBlockName(field)); const similarBlocks = findSimilarBlocks(result, field?.clientId); @@ -380,7 +390,7 @@ const extractItems = async (item: any, config: DataConfig, type: string, affix: // No duplicate found - add the modular block child if(Schema?.length > 0){ CT?.push?.({ - "uid": `modular_blocks.${getFieldUid(`${field?.name}_${field?.clientId}`, affix)}`, + "uid": `modular_blocks.${getFieldUid(`${field?.name}_${clientIdForUid(field?.clientId)}`, affix)}`, "backupFieldUid": `modular_blocks.${fieldUid}`, "contentstackFieldUid": `modular_blocks.${fieldUid}`, "otherCmsField": contentstackFieldName, @@ -436,7 +446,7 @@ const extractItems = async (item: any, config: DataConfig, type: string, affix: // No duplicate found - add the modular block child if(Schema?.length > 0){ CT?.push?.({ - "uid": `modular_blocks.${getFieldUid(`${field?.name}_${field?.clientId}`, affix)}`, + "uid": `modular_blocks.${getFieldUid(`${field?.name}_${clientIdForUid(field?.clientId)}`, affix)}`, "backupFieldUid": `modular_blocks.${fieldUid}`, "contentstackFieldUid": `modular_blocks.${fieldUid}`, "otherCmsField": contentstackFieldName, diff --git a/upload-api/migration-wordpress/libs/extractTaxonomy.ts b/upload-api/migration-wordpress/libs/extractTaxonomy.ts index 55b2f74c8..a1bf0c1bb 100644 --- a/upload-api/migration-wordpress/libs/extractTaxonomy.ts +++ b/upload-api/migration-wordpress/libs/extractTaxonomy.ts @@ -10,6 +10,7 @@ const handleTaxonomySchema = async(categories: any, allCategories : Categories[] taxonomyArray?.push( { "taxonomy_uid": `${categoryData?.["wp:category_nicename"]}_${categoryData?.["wp:term_id"]}`, + "taxonomy_name": categoryData?.["wp:cat_name"], "mandatory": false, "multiple": true, "non_localizable": false @@ -20,6 +21,7 @@ const handleTaxonomySchema = async(categories: any, allCategories : Categories[] const parentCategory = allCategories?.find((category: any) => category?.["wp:category_nicename"] === categoryData?.['wp:category_parent']); taxonomyArray?.push({ "taxonomy_uid": `${parentCategory?.["wp:category_nicename"]}_${parentCategory?.["wp:term_id"]}`, + "taxonomy_name": parentCategory?.["wp:cat_name"], "mandatory": false, "multiple": true, "non_localizable": false diff --git a/upload-api/migration-wordpress/libs/schemaMapper.ts b/upload-api/migration-wordpress/libs/schemaMapper.ts index 25328fad0..9bb88ee97 100644 --- a/upload-api/migration-wordpress/libs/schemaMapper.ts +++ b/upload-api/migration-wordpress/libs/schemaMapper.ts @@ -43,6 +43,13 @@ const getFieldUid = (key: string, affix: string) => { return isPresent ? `${affix}_${uid}` : uid; }; + +/** First 4 chars of clientId (hyphens stripped) — short UIDs; tiny collision risk on huge pages. */ +export function clientIdForUid(clientId: string | undefined): string { + if (!clientId) return '0'; + const compact = clientId?.replace?.(/-/g, '')?.toLowerCase(); + return compact?.slice?.(0, 4) || '0'; +} async function processInnerBlocks(key: WordPressBlock, parentUid: string | null = null, parentFieldName: string | null = null, affix: string | null = null): Promise { @@ -201,8 +208,8 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/verse': case 'core/code': { const rteUid = parentUid ? - `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` - : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` + : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); return { uid: rteUid, otherCmsField: getFieldName(key?.name), @@ -217,8 +224,8 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: } case 'core/missing': const rteUid = parentUid ? - `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` - : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` + : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); if(key?.attributes?.originalName === 'jetpack/markdown'){ return { uid: rteUid, @@ -248,7 +255,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/audio': case 'core/video': case 'core/file': { - const fileUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const fileUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); return { uid: fileUid, @@ -266,7 +273,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/heading': case 'core/accordion-heading': case 'core/list-item': { - const textUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const textUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); return { uid: textUid, otherCmsField: getFieldName(key?.name), @@ -283,7 +290,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/social-link': case 'core/navigation-link': { - const LinkUid = parentUid ? `${parentUid}.${getFieldUid(key?.name, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const LinkUid = parentUid ? `${parentUid}.${getFieldUid(key?.name, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); return { uid: LinkUid, otherCmsField: getFieldName(key?.name), @@ -307,7 +314,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/accordion-panel': case 'core/navigation': { const groupSchema: Field[] = []; - const groupUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const groupUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); const innerBlocks = await processInnerBlocks( key, @@ -346,7 +353,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: } case 'core/search': { - const searchEleUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const searchEleUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); const searchEle = await processAttributes(key, searchEleUid,fieldName, affix); const groupSchema: Field[] = []; searchEle?.length > 0 && groupSchema?.push({ @@ -374,7 +381,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/button': { const fieldName = parentFieldName ? `${parentFieldName} > ${getFieldName(key?.attributes?.metadata?.name ?? key?.name)}` : `${getFieldName(key?.attributes?.metadata?.name ?? key?.name)}` ; - const buttonUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const buttonUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); return { uid: buttonUid, @@ -392,7 +399,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: case 'core/buttons': { const groupSchema: Field[] = []; - const groupUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` : getFieldUid(`${key?.name}_${key?.clientId}`, affix); + const groupUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); const innerBlocks = await processInnerBlocks( key, @@ -402,11 +409,11 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: ); if (innerBlocks?.length === 1) { const items = Array.isArray(innerBlocks[0]) ? innerBlocks[0] : [innerBlocks[0]]; - items.forEach((item: Field) => { - item.uid = `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}`, + items?.forEach((item: Field) => { + item.uid = `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`; item.contentstackField = `${parentFieldName} > ${getFieldName(resolveBlockName(key))}`; - item.contentstackFieldUid = `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}`, - item.backupFieldUid = `${parentUid}.${getFieldUid(`${key?.name}_${key?.clientId}`, affix)}` + item.contentstackFieldUid = `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`; + item.backupFieldUid = `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`; }); return items; } @@ -444,4 +451,4 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: return []; } -export { getFieldName, getFieldUid ,schemaMapper, handleAttributesSchema}; \ No newline at end of file +export { getFieldName, getFieldUid, schemaMapper, handleAttributesSchema }; \ No newline at end of file diff --git a/upload-api/src/config/index.ts b/upload-api/src/config/index.ts index adf727923..4bde1a7e6 100644 --- a/upload-api/src/config/index.ts +++ b/upload-api/src/config/index.ts @@ -23,5 +23,5 @@ export default { base_url: process.env.DRUPAL_ASSETS_BASE_URL || 'drupal_assets_base_url', public_path: process.env.DRUPAL_ASSETS_PUBLIC_PATH || 'drupal_assets_public_path' }, - localPath: process.env.CMS_LOCAL_PATH || process.env.CONTAINER_PATH || 'localPath', + localPath: process.env.CMS_LOCAL_PATH || process.env.CONTAINER_PATH || 'your_local_cms_data_path', }; diff --git a/upload-api/src/services/contentful/index.ts b/upload-api/src/services/contentful/index.ts index 55db118e7..6a36ab389 100644 --- a/upload-api/src/services/contentful/index.ts +++ b/upload-api/src/services/contentful/index.ts @@ -1,11 +1,18 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import axios from 'axios'; +import fs from 'fs'; +import path from 'path'; import logger from '../../utils/logger'; import { HTTP_CODES, HTTP_TEXTS } from '../../constants'; import { Config } from '../../models/types'; -const { extractContentTypes, createInitialMapper, extractLocale } = require('migration-contentful'); +const { + extractContentTypes, + createInitialMapper, + extractLocale, + extractTaxonomy +} = require('migration-contentful'); const createContentfulMapper = async ( projectId: string | string[], @@ -20,6 +27,25 @@ const createContentfulMapper = async ( await extractContentTypes(cleanLocalPath, affix); const initialMapper = await createInitialMapper(cleanLocalPath, affix); + // Must run after createInitialMapper: that step deletes contentfulMigrationData (contentfulSchema) and would remove taxonomy files written earlier. + await extractTaxonomy(cleanLocalPath); + + let taxonomies: any[] = []; + try { + const taxonomyPath = path.join( + process.cwd(), + 'contentfulMigrationData', + 'taxonomySchema', + 'taxonomySchema.json' + ); + if (fs.existsSync(taxonomyPath)) { + const taxonomyData = await fs.promises.readFile(taxonomyPath, 'utf8'); + taxonomies = JSON.parse(taxonomyData); + logger.info(`Loaded ${taxonomies.length} Contentful taxonomies to send to API`); + } + } catch (error: any) { + logger.warn(`Could not read Contentful taxonomies: ${error.message}`); + } const req = { method: 'post', maxBodyLength: Infinity, @@ -28,7 +54,10 @@ const createContentfulMapper = async ( app_token, 'Content-Type': 'application/json' }, - data: JSON.stringify(initialMapper) + data: JSON.stringify({ + ...initialMapper, + taxonomies + }) }; const { data} = await axios.request(req); if (data?.data?.content_mapper?.length) { diff --git a/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts b/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts index 38f41ccf4..1c91d0b86 100644 --- a/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts +++ b/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts @@ -212,9 +212,9 @@ describe('schemaMapper', () => { const result = await schemaMapper(block, 'page', 'Page', affix); result.forEach((field: any) => { - expect(field.contentstackFieldUid).toBe(`page.${getFieldUid('core/buttons_btns1', affix)}`); - expect(field.uid).toBe(`page.${getFieldUid('core/buttons_btns1', affix)}`); - expect(field.backupFieldUid).toBe(`page.${getFieldUid('core/buttons_btns1', affix)}`); + expect(field.contentstackFieldUid).toBe(`page.${getFieldUid('core/buttons_btns', affix)}`); + expect(field.uid).toBe(`page.${getFieldUid('core/buttons_btns', affix)}`); + expect(field.backupFieldUid).toBe(`page.${getFieldUid('core/buttons_btns', affix)}`); expect(field.contentstackField).toContain('Page > '); }); }); From 00b2f3854bad851b3b80d4ff6d26066a3ad4efdd Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Thu, 23 Apr 2026 19:39:05 +0530 Subject: [PATCH 02/63] docs: update README with installation instructions and usage guidelines --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fa7e06ced..6ba74491b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,19 @@ Check for readme.md files and install dependencies for folders This is the migration V2's node server. +### Installation + +1. Navigate to the project directory: + + ```sh + cd api + ``` + +2. Install the dependencies: + ```sh + npm install + ``` + ### Environment Variables The following environment variables are used in this project: @@ -105,7 +118,7 @@ The migration-v2 upload-api project is designed to facilitate the migration of d Navigate to the project directory: ``` -cd migration-v2/upload-api +cd upload-api ``` Install dependencies: @@ -155,6 +168,14 @@ The following configuration is used in this project: - `npm run postinstall`: Installs dependencies for the api, ui, and upload-api directories. - `npm test`: Displays an error message indicating that no tests are specified. +### Usage + +Start the development server: + +```sh +npm start +``` + ## Repository - Type: git From cd96132e6589cae784305d4c85b21bb607f0a89a Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Thu, 23 Apr 2026 19:51:40 +0530 Subject: [PATCH 03/63] docs: clarify README environment variable instructions and add code block for server start command --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ba74491b..1ffb51323 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,16 @@ The following environment variables are used in this project: - `APP_TOKEN_KEY`: The token key for the application. Default is `MIGRATION_V2`. - `PORT`: The port number on which the application runs. Default is `5001`. -Make sure to set these variables in a `.env` file at the root of your project. +Make sure to set these variables in a `.env` file at the root of your api project. 1. To run the development server, create a `./development.env` file and add environment variables as per `./example.env` 2. To run the production server, create a `./production.env` file and add environment variables as per `./example.env` ### To start the server +```sh Run `npm run dev` +``` ## Migration UI @@ -134,7 +136,7 @@ The following environment variables are used in this project: - `PORT`: The port number on which the application runs. Default is `4002`. - `NODE_BACKEND_API`: The backend API endpoint. Default is `http://localhost:5001`. -Make sure to set these variables in a `.env` file at the root of your project. +Make sure to set these variables in a `.env` file at the root of your upload-api project. ### Configuration From d98107cb87b0cbbd6b9324ae565ebf6c2deccd2a Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Tue, 14 Apr 2026 12:56:07 +0530 Subject: [PATCH 04/63] refactor: improve select component with tooltip for existing field labels and enhance styling in ContentMapper --- ui/src/components/ContentMapper/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 99d73fb11..68d996012 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -2441,8 +2441,6 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: options={adjustedOptions} isDisabled={OptionValue?.isDisabled || newMigrationData?.project_current_step > 4} menuPlacement="auto" - menuPortalTarget={CONTENT_MAPPER_SELECT_MENU_PORTAL} - styles={contentMapperSelectMenuStyles} />
From e80b5158dc110d6615f9d09b31529aae084f0848 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Tue, 14 Apr 2026 19:20:10 +0530 Subject: [PATCH 05/63] feat: add utility function to find group fields in nested structures and integrate it into ContentMapper --- ui/src/components/ContentMapper/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 68d996012..da39e558b 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -2098,17 +2098,19 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: (opt: any) => opt?.label === newOption?.label && opt?.uid === newOption?.uid ); if (!isDuplicate) { + console.info("newOption --->", newOption, data?.contentstackField) OptionsForRow.push(newOption); } } } const existingLabel = existingField[groupArray?.[0]?.backupFieldUid]?.label ?? ''; + console.info("value ", value, existingLabel, groupArray?.[0]?.backupFieldUid) const lastLabelSegment = existingLabel?.includes('>') ? existingLabel?.split('>')?.pop()?.trim() : existingLabel; - + //console.info("existingLabel", existingLabel, lastLabelSegment) if (value?.display_name === lastLabelSegment) { const groupUid = groupArray?.[0]?.uid ?? ''; From d26c6288bc44b7f0fb34c3b0bc29815b46ef1f59 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Thu, 16 Apr 2026 15:15:34 +0530 Subject: [PATCH 06/63] refactor: removed console and added optional chaining --- ui/src/components/ContentMapper/index.tsx | 10 +++++----- upload-api/migration-wordpress/libs/extractItems.ts | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index da39e558b..43834610f 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -1510,7 +1510,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: return (
- 4} menuPlacement="auto" + menuPortalTarget={CONTENT_MAPPER_SELECT_MENU_PORTAL} + styles={contentMapperSelectMenuStyles} />
diff --git a/upload-api/migration-wordpress/libs/extractItems.ts b/upload-api/migration-wordpress/libs/extractItems.ts index 3b31c8aeb..17ab8bc17 100644 --- a/upload-api/migration-wordpress/libs/extractItems.ts +++ b/upload-api/migration-wordpress/libs/extractItems.ts @@ -269,6 +269,7 @@ const extractItems = async (item: any, config: DataConfig, type: string, affix: // Create the content type directory if it doesn't exist mkdirp(contentTypeFolderPath); + mkdirp.sync(blocksJsonOutputDir); //const category = await extractTaxonomy(categories, 'categories'); const categoryArray: Field = From 2af2919f9224baf1a1bdedcf778f2c07237a1ccc Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Fri, 17 Apr 2026 14:09:27 +0530 Subject: [PATCH 07/63] refactor: enhance type handling in remapReferenceUids function and improve optional chaining in AdvancePropertise and ContentMapper components --- ui/src/components/ContentMapper/index.tsx | 4 ++-- upload-api/migration-wordpress/libs/extractItems.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 43834610f..99d73fb11 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -1510,7 +1510,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: return (
- handleValueChange(selectedOption, data?.uid, data?.contentstackFieldUid)} @@ -2423,7 +2423,7 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: position="top" disabled={!selectValueIsExistingField} > - Date: Mon, 20 Apr 2026 18:07:47 +0530 Subject: [PATCH 08/63] refactor: optimize global field reference handling in convertToSchemaFormate function --- api/src/utils/content-type-creator.utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/utils/content-type-creator.utils.ts b/api/src/utils/content-type-creator.utils.ts index ddcdd3eda..9c0bc83c5 100644 --- a/api/src/utils/content-type-creator.utils.ts +++ b/api/src/utils/content-type-creator.utils.ts @@ -764,10 +764,11 @@ export const convertToSchemaFormate = ({ field, advanced = false, marketPlacePat case 'global_field': { + const globalFieldRefs = remapReferenceUids(field?.refrenceTo ?? [], keyMapper); return { "data_type": "global_field", "display_name": field?.title, - "reference_to": remapReferenceUids(field?.refrenceTo ?? [], keyMapper), + "reference_to": globalFieldRefs?.length === 1 ? globalFieldRefs?.[0] : globalFieldRefs, "uid": cleanedUid, "mandatory": field?.advanced?.mandatory ?? false, "multiple": field?.advanced?.multiple ?? false, From 761523587d11d0e802d4a777fdfced80f660e92d Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Tue, 21 Apr 2026 12:22:10 +0530 Subject: [PATCH 09/63] fix: correct error message formatting in LoadStacks component by adjusting data reference --- ui/src/components/DestinationStack/Actions/LoadStacks.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx index e52284c24..a176e27ea 100644 --- a/ui/src/components/DestinationStack/Actions/LoadStacks.tsx +++ b/ui/src/components/DestinationStack/Actions/LoadStacks.tsx @@ -184,7 +184,7 @@ const LoadStacks = (props: LoadFileFormatProps) => { return true; } else { - const errorMessage = formatErrorMessage(resp?.data?.data); + const errorMessage = formatErrorMessage(resp?.data); return errorMessage; } } catch (error: any) { From 6c1670c2085e9b040aff5cf0908b7f4c867b2017 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Wed, 22 Apr 2026 11:57:35 +0530 Subject: [PATCH 10/63] refactor: implement default widget ID inference and enhance field mapping resolution in Contentful service --- api/src/services/contentful.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/src/services/contentful.service.ts b/api/src/services/contentful.service.ts index d5ecc13f6..cd94ffb4c 100644 --- a/api/src/services/contentful.service.ts +++ b/api/src/services/contentful.service.ts @@ -215,7 +215,6 @@ function inferContentfulDefaultWidgetId(fieldType: string | undefined): string | return undefined; } } - function getContentfulFieldFromPackage( contentTypesFromPackage: any[] | undefined, ctId: string, @@ -224,7 +223,6 @@ function getContentfulFieldFromPackage( const ct = contentTypesFromPackage?.find((c: any) => c?.sys?.id === ctId); return ct?.fields?.find((f: any) => f?.id === fieldId); } - /** * Picks one fieldMapping row when several share the same `uid` (e.g. bootstrap `title`/`url` rows * from createInitialMapper plus the real Contentful field). Mapper `otherCmsType` is Contentful @@ -239,14 +237,12 @@ function resolveFieldMappingRow( const candidates = fieldMapping?.filter((item: any) => item?.uid === fieldId) ?? []; if (candidates?.length === 0) return undefined; if (candidates?.length === 1) return candidates?.[0]; - const cfField = getContentfulFieldFromPackage(contentTypesFromPackage, ctId, fieldId); const widgetId = cfField?.widgetId ?? inferContentfulDefaultWidgetId(cfField?.type); if (widgetId) { const byWidget = candidates?.filter((c: any) => c?.otherCmsType === widgetId); if (byWidget?.length >= 1) return byWidget?.[0]; } - const typeToCs: Record = { RichText: "json", Boolean: "boolean", @@ -257,7 +253,6 @@ function resolveFieldMappingRow( const byCs = candidates?.filter((c: any) => c?.contentstackFieldType === expectCs); if (byCs?.length >= 1) return byCs?.[0]; } - if (cfField?.type === "Boolean") { const byBool = candidates?.filter((c: any) => c?.contentstackFieldType === "boolean"); if (byBool?.length >= 1) return byBool?.[0]; From ab7747247166b04ad1a4475ad4f628c26d0cda02 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Fri, 24 Apr 2026 13:13:25 +0530 Subject: [PATCH 11/63] feat: add AutoMappedMergeConfirmModal component and enhance SaveChangesModal with async handling for step changes --- .../AutoMappedMergeConfirmModal/index.tsx | 43 ++ .../Common/SaveChangesModal/index.tsx | 12 +- .../ContentMapper/contentMapper.interface.ts | 2 + ui/src/components/ContentMapper/index.scss | 80 +++- ui/src/components/ContentMapper/index.tsx | 434 ++++++++++++++++-- .../HorizontalStepper/HorizontalStepper.tsx | 22 +- ui/src/context/app/app.interface.ts | 4 +- ui/src/pages/Migration/index.tsx | 48 +- ui/src/utilities/constants.ts | 9 +- ui/tests/unit/utilities/constants.test.ts | 2 + 10 files changed, 606 insertions(+), 50 deletions(-) create mode 100644 ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx diff --git a/ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx b/ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx new file mode 100644 index 000000000..f223a555f --- /dev/null +++ b/ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx @@ -0,0 +1,43 @@ +import { + ModalBody, + ModalHeader, + ModalFooter, + ButtonGroup, + Button +} from '@contentstack/venus-components'; + +interface Props { + closeModal: () => void; + onContinue: () => void | Promise; +} + +const AutoMappedMergeConfirmModal = (props: Props) => { + return ( + <> + props.closeModal()} + /> + + All auto-mapped content types will be merged into your exisitng content types and global fields. You can cancel to stay on this step or continue to the next step. + + + + + + + + + ); +}; + +export default AutoMappedMergeConfirmModal; diff --git a/ui/src/components/Common/SaveChangesModal/index.tsx b/ui/src/components/Common/SaveChangesModal/index.tsx index 67b3104d2..6c299da5e 100644 --- a/ui/src/components/Common/SaveChangesModal/index.tsx +++ b/ui/src/components/Common/SaveChangesModal/index.tsx @@ -13,7 +13,7 @@ interface Props { otherCmsTitle?: string; saveContentType?: () => void; openContentType?: () => void; - changeStep?: () => void; + changeStep?: () => void | Promise; dropdownStateChange: () => void; } @@ -47,24 +47,24 @@ const SaveChangesModal = (props: Props) => { ))} @@ -3190,6 +3516,34 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
    {filteredContentTypes?.map?.((content: ContentType, index: number) => { const icon = STATUS_ICON_Mapping[content?.status] || ''; + const existingCTSide = asContentTypeListArray( + newMigrationData?.content_mapping?.existingCT + ); + const existingGlobalSide = asContentTypeListArray( + newMigrationData?.content_mapping?.existingGlobal + ); + let rowDestinationModels: ContentTypeList[] = getDestinationModelsForRow( + content, + existingCTSide, + existingGlobalSide + ); + if ( + !rowDestinationModels?.length && + contentModels?.length && + selectedContentType?.contentstackUid === content?.contentstackUid && + ((content?.type === 'content_type' && isContentType) || + (content?.type !== 'content_type' && !isContentType)) + ) { + rowDestinationModels = contentModels; + } + const showAutoMappedBadge = + !isNewStack && + isContentTypeAutoMapped( + content?.contentstackUid, + rowDestinationModels, + combinedContentTypeMapping, + uidAutoMapSuppressedForSourceUids + ); const format = (str: string) => { const frags = str?.split('_'); @@ -3227,14 +3581,38 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
- + + {showAutoMappedBadge && ( + + e.preventDefault()} + > + + + + )} {icon && ( )} - + diff --git a/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx b/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx index 3a1be8f44..1660d16d3 100644 --- a/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx +++ b/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx @@ -44,6 +44,7 @@ export type stepperProps = { stepTitleClassName?: string; testId?: string; handleSaveCT?: () => void; + handleUpdateAutoMappedContentMapping?: () => Promise; changeDropdownState: () => void; projectData: MigrationResponse; isProjectMapped: boolean; @@ -82,7 +83,7 @@ const HorizontalStepper = forwardRef( const newMigrationData = useSelector((state: RootState) => state?.migration?.newMigrationData); - const { steps, className, emptyStateMsg, hideTabView, testId } = props; + const { steps, className, emptyStateMsg, hideTabView, testId, handleUpdateAutoMappedContentMapping } = props; const [showStep, setShowStep] = useState(stepIndex); const [stepsCompleted, setStepsCompleted] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); @@ -144,13 +145,26 @@ const HorizontalStepper = forwardRef( if (newMigrationData?.content_mapping?.isDropDownChanged) { setIsModalOpen(true); return cbModal({ - component: (props: ModalObj) => ( + component: (modalProps: ModalObj) => ( setTabStep(idx)} + changeStep={async () => { + try { + await handleUpdateAutoMappedContentMapping?.(); + } catch { + Notification({ + notificationContent: { + text: 'Could not save content type mapping. Please try again.' + }, + type: 'error' + }); + return; + } + setTabStep(idx); + }} dropdownStateChange={handleDropdownChange} /> ), diff --git a/ui/src/context/app/app.interface.ts b/ui/src/context/app/app.interface.ts index 80ef579fd..34c0df667 100644 --- a/ui/src/context/app/app.interface.ts +++ b/ui/src/context/app/app.interface.ts @@ -191,8 +191,8 @@ export interface IDestinationStack { csLocale: string[]; } export interface IContentMapper { - existingGlobal: ContentTypeList[] | (() => ContentTypeList[]); - existingCT: ContentTypeList[] | (() => ContentTypeList[]); + existingGlobal: ContentTypeList[]; + existingCT: ContentTypeList[]; content_type_mapping: ContentTypeMap; isDropDownChanged?: boolean; otherCmsTitle?: string; diff --git a/ui/src/pages/Migration/index.tsx b/ui/src/pages/Migration/index.tsx index 7dc709045..064bc4469 100644 --- a/ui/src/pages/Migration/index.tsx +++ b/ui/src/pages/Migration/index.tsx @@ -60,6 +60,7 @@ import ContentMapper from '../../components/ContentMapper'; import TestMigration from '../../components/TestMigration'; import MigrationExecution from '../../components/MigrationExecution'; import SaveChangesModal from '../../components/Common/SaveChangesModal'; +import AutoMappedMergeConfirmModal from '../../components/Common/AutoMappedMergeConfirmModal'; import { getMigratedStacks } from '../../services/api/project.service'; import { getConfig } from '../../services/api/upload.service'; import { useWarnOnRefresh } from '../../hooks/useWarnOnrefresh'; @@ -749,6 +750,22 @@ const Migration = () => { * Calls when click Continue button on Content Mapper step and handles to proceed to Test Migration */ const handleOnClickContentMapper = async (event: MouseEvent) => { + const persistAutoMappedContentMapper = async (): Promise => { + try { + await saveRef?.current?.handleUpdateAutoMappedContentMapping?.(); + return true; + } catch { + Notification({ + notificationContent: { + text: 'Could not save content type mapping. Please try again.' + }, + notificationProps: { position: 'bottom-center', hideProgressBar: true }, + type: 'error' + }); + return false; + } + }; + if (newMigrationData?.content_mapping?.isDropDownChanged) { setIsModalOpen(true); @@ -760,6 +777,7 @@ const Migration = () => { otherCmsTitle={newMigrationData?.content_mapping?.otherCmsTitle} saveContentType={saveRef?.current?.handleSaveContentType} changeStep={async () => { + if (!(await persistAutoMappedContentMapper())) return; const url = `/projects/${projectId}/migration/steps/4`; navigate(url, { replace: true }); @@ -775,14 +793,35 @@ const Migration = () => { } }); } else { - - const res = await updateCurrentStepData(selectedOrganisation.value, projectId); + const finishContentMapperNavigation = async () => { + if (!(await persistAutoMappedContentMapper())) return; + await updateCurrentStepData(selectedOrganisation.value, projectId); setIsLoading(false); - event.preventDefault(); + event?.preventDefault?.(); handleStepChange(3); const url = `/projects/${projectId}/migration/steps/4`; navigate(url, { replace: true }); + }; + + if (saveRef?.current?.shouldPromptShowAutoMappedMerge?.()) { + return cbModal({ + component: (props: ModalObj) => ( + { + props.closeModal(); + await finishContentMapperNavigation(); + }} + /> + ), + modalProps: { + size: 'xsmall', + shouldCloseOnOverlayClick: false + } + }); + } + await finishContentMapperNavigation(); } }; @@ -878,6 +917,9 @@ const Migration = () => { ref={stepperRef} steps={createStepper(projectData ?? defaultMigrationResponse, handleStepChange)} handleSaveCT={saveRef?.current?.handleSaveContentType} + handleUpdateAutoMappedContentMapping={() => + saveRef?.current?.handleUpdateAutoMappedContentMapping?.() ?? Promise.resolve() + } changeDropdownState={changeDropdownState} projectData={projectData || defaultMigrationResponse} isProjectMapped={isProjectMapper} diff --git a/ui/src/utilities/constants.ts b/ui/src/utilities/constants.ts index 403f7a508..6f831b92e 100644 --- a/ui/src/utilities/constants.ts +++ b/ui/src/utilities/constants.ts @@ -105,14 +105,13 @@ export const CONTENT_MAPPING_STATUS: ObjectType = { '1': 'Mapped', '2': 'Updated', '3': 'Failed', - '4': 'All' - // '4': 'Auto-Dump' + '4': 'All', + '5': 'Auto-mapped', }; export const STATUS_ICON_Mapping: { [key: string]: string } = { '1': 'CheckedCircle', '2': 'SuccessInverted', - '3': 'ErrorInverted' - // '4': 'completed' + '3': 'ErrorInverted', }; export const VALIDATION_DOCUMENTATION_URL: { [key: string]: string } = { @@ -199,3 +198,5 @@ export const EXECUTION_LOGS_UI_TEXT = { export const EXECUTION_LOGS_ERROR_TEXT = { ERROR: 'Error in Getting Migration Logs' } + +export const AUTO_MAPPED_PILL_ITEMS = [{ id: 'auto-mapped', text: 'Auto-mapped' }]; \ No newline at end of file diff --git a/ui/tests/unit/utilities/constants.test.ts b/ui/tests/unit/utilities/constants.test.ts index ee2c057f0..2a15360f3 100644 --- a/ui/tests/unit/utilities/constants.test.ts +++ b/ui/tests/unit/utilities/constants.test.ts @@ -111,6 +111,7 @@ describe('utilities/constants', () => { expect(CONTENT_MAPPING_STATUS['1']).toBe('Mapped'); expect(CONTENT_MAPPING_STATUS['2']).toBe('Updated'); expect(CONTENT_MAPPING_STATUS['3']).toBe('Failed'); + expect(CONTENT_MAPPING_STATUS['5']).toBe('Auto-mapped'); expect(CONTENT_MAPPING_STATUS['4']).toBe('All'); }); @@ -118,6 +119,7 @@ describe('utilities/constants', () => { expect(STATUS_ICON_Mapping['1']).toBe('CheckedCircle'); expect(STATUS_ICON_Mapping['2']).toBe('SuccessInverted'); expect(STATUS_ICON_Mapping['3']).toBe('ErrorInverted'); + expect(STATUS_ICON_Mapping['5']).toBe('Link'); }); it('should export VALIDATION_DOCUMENTATION_URL', () => { From 31d659357ae10364a9718f57b7fbd8f22ce51126 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Fri, 24 Apr 2026 13:17:46 +0530 Subject: [PATCH 12/63] fix: remove incorrect mapping for STATUS_ICON_Mapping in constants test --- ui/tests/unit/utilities/constants.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/tests/unit/utilities/constants.test.ts b/ui/tests/unit/utilities/constants.test.ts index 2a15360f3..4a77aefc7 100644 --- a/ui/tests/unit/utilities/constants.test.ts +++ b/ui/tests/unit/utilities/constants.test.ts @@ -119,7 +119,6 @@ describe('utilities/constants', () => { expect(STATUS_ICON_Mapping['1']).toBe('CheckedCircle'); expect(STATUS_ICON_Mapping['2']).toBe('SuccessInverted'); expect(STATUS_ICON_Mapping['3']).toBe('ErrorInverted'); - expect(STATUS_ICON_Mapping['5']).toBe('Link'); }); it('should export VALIDATION_DOCUMENTATION_URL', () => { From 2c9583a0169966ae40cb2709ca9c05371a222176 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Fri, 24 Apr 2026 13:31:13 +0530 Subject: [PATCH 13/63] chore: update uuid package from version 9.0.1 to 14.0.0 across multiple package.json and package-lock.json files --- api/package-lock.json | 97 +++++++++++++++++++--- api/package.json | 1 - upload-api/migration-aem/package-lock.json | 11 ++- upload-api/migration-aem/package.json | 3 +- 4 files changed, 90 insertions(+), 22 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index e219bca0d..6790d04b2 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -40,7 +40,7 @@ "p-limit": "^6.2.0", "php-serialize": "^5.1.3", "socket.io": "^4.7.5", - "uuid": "^9.0.1", + "uuid": "^14.0.0", "winston": "^3.11.0" }, "devDependencies": { @@ -54,7 +54,6 @@ "@types/lodash": "^4.17.0", "@types/node": "^20.10.4", "@types/supertest": "^6.0.3", - "@types/uuid": "^9.0.8", "@types/wordpress__block-library": "^2.6.3", "@types/wordpress__block-serialization-spec-parser": "^3.1.3", "@types/wordpress__blocks": "^12.5.18", @@ -407,6 +406,19 @@ "node": ">=16" } }, + "node_modules/@contentstack/cli-audit/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/cli-auth": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@contentstack/cli-auth/-/cli-auth-1.8.0.tgz", @@ -648,6 +660,19 @@ "node": ">=10" } }, + "node_modules/@contentstack/cli-cm-import/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/cli-cm-migrate-rte": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/@contentstack/cli-cm-migrate-rte/-/cli-cm-migrate-rte-1.6.4.tgz", @@ -917,6 +942,19 @@ "node": ">=12" } }, + "node_modules/@contentstack/cli-cm-migrate-rte/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/cli-cm-migrate-rte/node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -1143,6 +1181,19 @@ "node": ">=10" } }, + "node_modules/@contentstack/cli-utilities/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/cli-variants": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@contentstack/cli-variants/-/cli-variants-1.4.1.tgz", @@ -1169,6 +1220,19 @@ "node": ">=10" } }, + "node_modules/@contentstack/cli/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/json-rte-serializer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@contentstack/json-rte-serializer/-/json-rte-serializer-3.0.5.tgz", @@ -4048,13 +4112,6 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/wordpress__block-library": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/@types/wordpress__block-library/-/wordpress__block-library-2.6.3.tgz", @@ -4522,6 +4579,20 @@ "react-dom": "^18.0.0" } }, + "node_modules/@wordpress/components/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/compose": { "version": "6.35.0", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-6.35.0.tgz", @@ -17567,16 +17638,16 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/validate-npm-package-name": { diff --git a/api/package.json b/api/package.json index d75f4be29..fab2384d5 100644 --- a/api/package.json +++ b/api/package.json @@ -75,7 +75,6 @@ "@types/lodash": "^4.17.0", "@types/node": "^20.10.4", "@types/supertest": "^6.0.3", - "@types/uuid": "^9.0.8", "@types/wordpress__block-library": "^2.6.3", "@types/wordpress__block-serialization-spec-parser": "^3.1.3", "@types/wordpress__blocks": "^12.5.18", diff --git a/upload-api/migration-aem/package-lock.json b/upload-api/migration-aem/package-lock.json index 79a596151..95c16d44e 100644 --- a/upload-api/migration-aem/package-lock.json +++ b/upload-api/migration-aem/package-lock.json @@ -11,11 +11,10 @@ "dependencies": { "fs-readdir-recursive": "^1.1.0", "genson-js": "^0.0.8", - "uuid": "^9.0.1" + "uuid": "^14.0.0" }, "devDependencies": { "@types/fs-readdir-recursive": "^1.1.3", - "@types/uuid": "^9.0.8", "nodemon": "^3.1.10", "rimraf": "^6.0.1", "ts-node": "^10.9.2", @@ -775,16 +774,16 @@ "peer": true }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/upload-api/migration-aem/package.json b/upload-api/migration-aem/package.json index bab9fc6e4..14529a4d3 100644 --- a/upload-api/migration-aem/package.json +++ b/upload-api/migration-aem/package.json @@ -15,7 +15,6 @@ "license": "ISC", "devDependencies": { "@types/fs-readdir-recursive": "^1.1.3", - "@types/uuid": "^9.0.8", "nodemon": "^3.1.10", "rimraf": "^6.0.1", "ts-node": "^10.9.2", @@ -24,6 +23,6 @@ "dependencies": { "fs-readdir-recursive": "^1.1.0", "genson-js": "^0.0.8", - "uuid": "^9.0.1" + "uuid": "^14.0.0" } } \ No newline at end of file From 6f554725036feeea3cb14e6810e339812619b437 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Fri, 24 Apr 2026 17:21:51 +0530 Subject: [PATCH 14/63] refactor: improve RteJsonConverter and block name resolution logic for better handling of missing and media blocks --- api/src/services/wordpress.service.ts | 42 ++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/api/src/services/wordpress.service.ts b/api/src/services/wordpress.service.ts index 628c980a4..e8f6debeb 100644 --- a/api/src/services/wordpress.service.ts +++ b/api/src/services/wordpress.service.ts @@ -106,7 +106,10 @@ const getFieldName = (key: string ) => { } const RteJsonConverter = (html: string) => { - const dom = new JSDOM(html); + const cleanedHtml = html + ?.replace(/]*>/g, "") + ?.replace(/<\/figure>/g, ""); + const dom = new JSDOM(cleanedHtml); const htmlDoc = dom.window.document.querySelector("body"); return htmlToJson(htmlDoc); @@ -127,13 +130,30 @@ function getLastUid(uid : string) { const resolvedBlockName = (block: any) => { - if (block?.attrs?.metadata?.name) return block?.attrs?.metadata?.name; - if (block?.blockName === WORDPRESS_MISSSING_BLOCKS) { - return block?.attrs?.originalName || 'body'; + // 1. If metadata name exists, use it first + if (block?.attrs?.metadata?.name) { + return block.attrs.metadata.name; } - if (MEDIA_BLOCK_NAMES?.includes?.(block?.blockName)) return 'media'; + + // 2. Handle missing/invalid WordPress blocks + const isMissingBlock = + block?.blockName === WORDPRESS_MISSSING_BLOCKS || + (block?.blockName === null && + block?.innerHTML !== ' '); + if (isMissingBlock) { + // fallback to originalName, otherwise use body + + return block?.attrs?.originalName ?? "body"; + } + + // 3. Handle media-related blocks + if (MEDIA_BLOCK_NAMES?.includes?.(block?.blockName)) { + return "media"; + } + + // 4. Default fallback return block?.blockName; -} +}; async function createSchema(fields: any, blockJson : any, title: string, uid: string, assetData: any, duplicateBlockMappings?: Record) { const schema : any = { @@ -360,7 +380,7 @@ function processNestedGroup(child: any, childField: any, allFields: any[]): Reco nestedChildrenObject[nestedChildKey] = [formattedNestedChild]; } } else { - nestedChildrenObject[nestedChildKey] = formattedNestedChild; + formattedNestedChild && (nestedChildrenObject[nestedChildKey] = formattedNestedChild); } } } catch (nestedError) { @@ -438,9 +458,12 @@ function formatChildByType(child: any, field: any, assetData: any) { htmlContent = collectHtmlFromInnerBlocks(child); } if (!htmlContent) { - htmlContent = child?.blockName ? child?.innerHTML : child; + htmlContent = (child?.blockName || child?.innerHTML) + ? child?.innerHTML + : child; } - formatted = RteJsonConverter(htmlContent); + const hasMeaningfulHtml = stripHtmlTags(htmlContent)?.trim()?.length > 0; + formatted = hasMeaningfulHtml && RteJsonConverter(htmlContent); break; } @@ -601,7 +624,6 @@ async function saveEntry(fields: any, entry: any, file_path: string, assetData customLogger(project?.id, destinationStackId,'info', `Processed blocks for entry ${uid}`); - // Pass individual content to createSchema entryData[uid] = await createSchema(fields, blocksJson, item?.title, uid, assetData, duplicateBlockMappings); From e68c6ca8c26540f2ae4988d619586305699041d1 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Mon, 27 Apr 2026 11:14:07 +0530 Subject: [PATCH 15/63] Revert "feat: add AutoMappedMergeConfirmModal component and enhance SaveChangesModal with async handling for step changes" This reverts commit fe3bfed302148f6bf9be9463442e70cebd6a9a7a. --- .../AutoMappedMergeConfirmModal/index.tsx | 43 -- .../Common/SaveChangesModal/index.tsx | 12 +- .../ContentMapper/contentMapper.interface.ts | 2 - ui/src/components/ContentMapper/index.scss | 80 +--- ui/src/components/ContentMapper/index.tsx | 434 ++---------------- .../HorizontalStepper/HorizontalStepper.tsx | 22 +- ui/src/context/app/app.interface.ts | 4 +- ui/src/pages/Migration/index.tsx | 48 +- ui/src/utilities/constants.ts | 9 +- ui/tests/unit/utilities/constants.test.ts | 1 - 10 files changed, 50 insertions(+), 605 deletions(-) delete mode 100644 ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx diff --git a/ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx b/ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx deleted file mode 100644 index f223a555f..000000000 --- a/ui/src/components/Common/AutoMappedMergeConfirmModal/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - ModalBody, - ModalHeader, - ModalFooter, - ButtonGroup, - Button -} from '@contentstack/venus-components'; - -interface Props { - closeModal: () => void; - onContinue: () => void | Promise; -} - -const AutoMappedMergeConfirmModal = (props: Props) => { - return ( - <> - props.closeModal()} - /> - - All auto-mapped content types will be merged into your exisitng content types and global fields. You can cancel to stay on this step or continue to the next step. - - - - - - - - - ); -}; - -export default AutoMappedMergeConfirmModal; diff --git a/ui/src/components/Common/SaveChangesModal/index.tsx b/ui/src/components/Common/SaveChangesModal/index.tsx index 6c299da5e..67b3104d2 100644 --- a/ui/src/components/Common/SaveChangesModal/index.tsx +++ b/ui/src/components/Common/SaveChangesModal/index.tsx @@ -13,7 +13,7 @@ interface Props { otherCmsTitle?: string; saveContentType?: () => void; openContentType?: () => void; - changeStep?: () => void | Promise; + changeStep?: () => void; dropdownStateChange: () => void; } @@ -47,24 +47,24 @@ const SaveChangesModal = (props: Props) => { ))} @@ -3516,34 +3190,6 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
    {filteredContentTypes?.map?.((content: ContentType, index: number) => { const icon = STATUS_ICON_Mapping[content?.status] || ''; - const existingCTSide = asContentTypeListArray( - newMigrationData?.content_mapping?.existingCT - ); - const existingGlobalSide = asContentTypeListArray( - newMigrationData?.content_mapping?.existingGlobal - ); - let rowDestinationModels: ContentTypeList[] = getDestinationModelsForRow( - content, - existingCTSide, - existingGlobalSide - ); - if ( - !rowDestinationModels?.length && - contentModels?.length && - selectedContentType?.contentstackUid === content?.contentstackUid && - ((content?.type === 'content_type' && isContentType) || - (content?.type !== 'content_type' && !isContentType)) - ) { - rowDestinationModels = contentModels; - } - const showAutoMappedBadge = - !isNewStack && - isContentTypeAutoMapped( - content?.contentstackUid, - rowDestinationModels, - combinedContentTypeMapping, - uidAutoMapSuppressedForSourceUids - ); const format = (str: string) => { const frags = str?.split('_'); @@ -3581,38 +3227,14 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref:
- - {showAutoMappedBadge && ( - - e.preventDefault()} - > - - - - )} + {icon && ( )} - + diff --git a/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx b/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx index 1660d16d3..3a1be8f44 100644 --- a/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx +++ b/ui/src/components/Stepper/HorizontalStepper/HorizontalStepper.tsx @@ -44,7 +44,6 @@ export type stepperProps = { stepTitleClassName?: string; testId?: string; handleSaveCT?: () => void; - handleUpdateAutoMappedContentMapping?: () => Promise; changeDropdownState: () => void; projectData: MigrationResponse; isProjectMapped: boolean; @@ -83,7 +82,7 @@ const HorizontalStepper = forwardRef( const newMigrationData = useSelector((state: RootState) => state?.migration?.newMigrationData); - const { steps, className, emptyStateMsg, hideTabView, testId, handleUpdateAutoMappedContentMapping } = props; + const { steps, className, emptyStateMsg, hideTabView, testId } = props; const [showStep, setShowStep] = useState(stepIndex); const [stepsCompleted, setStepsCompleted] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); @@ -145,26 +144,13 @@ const HorizontalStepper = forwardRef( if (newMigrationData?.content_mapping?.isDropDownChanged) { setIsModalOpen(true); return cbModal({ - component: (modalProps: ModalObj) => ( + component: (props: ModalObj) => ( { - try { - await handleUpdateAutoMappedContentMapping?.(); - } catch { - Notification({ - notificationContent: { - text: 'Could not save content type mapping. Please try again.' - }, - type: 'error' - }); - return; - } - setTabStep(idx); - }} + changeStep={() => setTabStep(idx)} dropdownStateChange={handleDropdownChange} /> ), diff --git a/ui/src/context/app/app.interface.ts b/ui/src/context/app/app.interface.ts index 34c0df667..80ef579fd 100644 --- a/ui/src/context/app/app.interface.ts +++ b/ui/src/context/app/app.interface.ts @@ -191,8 +191,8 @@ export interface IDestinationStack { csLocale: string[]; } export interface IContentMapper { - existingGlobal: ContentTypeList[]; - existingCT: ContentTypeList[]; + existingGlobal: ContentTypeList[] | (() => ContentTypeList[]); + existingCT: ContentTypeList[] | (() => ContentTypeList[]); content_type_mapping: ContentTypeMap; isDropDownChanged?: boolean; otherCmsTitle?: string; diff --git a/ui/src/pages/Migration/index.tsx b/ui/src/pages/Migration/index.tsx index 064bc4469..7dc709045 100644 --- a/ui/src/pages/Migration/index.tsx +++ b/ui/src/pages/Migration/index.tsx @@ -60,7 +60,6 @@ import ContentMapper from '../../components/ContentMapper'; import TestMigration from '../../components/TestMigration'; import MigrationExecution from '../../components/MigrationExecution'; import SaveChangesModal from '../../components/Common/SaveChangesModal'; -import AutoMappedMergeConfirmModal from '../../components/Common/AutoMappedMergeConfirmModal'; import { getMigratedStacks } from '../../services/api/project.service'; import { getConfig } from '../../services/api/upload.service'; import { useWarnOnRefresh } from '../../hooks/useWarnOnrefresh'; @@ -750,22 +749,6 @@ const Migration = () => { * Calls when click Continue button on Content Mapper step and handles to proceed to Test Migration */ const handleOnClickContentMapper = async (event: MouseEvent) => { - const persistAutoMappedContentMapper = async (): Promise => { - try { - await saveRef?.current?.handleUpdateAutoMappedContentMapping?.(); - return true; - } catch { - Notification({ - notificationContent: { - text: 'Could not save content type mapping. Please try again.' - }, - notificationProps: { position: 'bottom-center', hideProgressBar: true }, - type: 'error' - }); - return false; - } - }; - if (newMigrationData?.content_mapping?.isDropDownChanged) { setIsModalOpen(true); @@ -777,7 +760,6 @@ const Migration = () => { otherCmsTitle={newMigrationData?.content_mapping?.otherCmsTitle} saveContentType={saveRef?.current?.handleSaveContentType} changeStep={async () => { - if (!(await persistAutoMappedContentMapper())) return; const url = `/projects/${projectId}/migration/steps/4`; navigate(url, { replace: true }); @@ -793,35 +775,14 @@ const Migration = () => { } }); } else { - const finishContentMapperNavigation = async () => { - if (!(await persistAutoMappedContentMapper())) return; - await updateCurrentStepData(selectedOrganisation.value, projectId); + + const res = await updateCurrentStepData(selectedOrganisation.value, projectId); setIsLoading(false); - event?.preventDefault?.(); + event.preventDefault(); handleStepChange(3); const url = `/projects/${projectId}/migration/steps/4`; navigate(url, { replace: true }); - }; - - if (saveRef?.current?.shouldPromptShowAutoMappedMerge?.()) { - return cbModal({ - component: (props: ModalObj) => ( - { - props.closeModal(); - await finishContentMapperNavigation(); - }} - /> - ), - modalProps: { - size: 'xsmall', - shouldCloseOnOverlayClick: false - } - }); - } - await finishContentMapperNavigation(); } }; @@ -917,9 +878,6 @@ const Migration = () => { ref={stepperRef} steps={createStepper(projectData ?? defaultMigrationResponse, handleStepChange)} handleSaveCT={saveRef?.current?.handleSaveContentType} - handleUpdateAutoMappedContentMapping={() => - saveRef?.current?.handleUpdateAutoMappedContentMapping?.() ?? Promise.resolve() - } changeDropdownState={changeDropdownState} projectData={projectData || defaultMigrationResponse} isProjectMapped={isProjectMapper} diff --git a/ui/src/utilities/constants.ts b/ui/src/utilities/constants.ts index 6f831b92e..403f7a508 100644 --- a/ui/src/utilities/constants.ts +++ b/ui/src/utilities/constants.ts @@ -105,13 +105,14 @@ export const CONTENT_MAPPING_STATUS: ObjectType = { '1': 'Mapped', '2': 'Updated', '3': 'Failed', - '4': 'All', - '5': 'Auto-mapped', + '4': 'All' + // '4': 'Auto-Dump' }; export const STATUS_ICON_Mapping: { [key: string]: string } = { '1': 'CheckedCircle', '2': 'SuccessInverted', - '3': 'ErrorInverted', + '3': 'ErrorInverted' + // '4': 'completed' }; export const VALIDATION_DOCUMENTATION_URL: { [key: string]: string } = { @@ -198,5 +199,3 @@ export const EXECUTION_LOGS_UI_TEXT = { export const EXECUTION_LOGS_ERROR_TEXT = { ERROR: 'Error in Getting Migration Logs' } - -export const AUTO_MAPPED_PILL_ITEMS = [{ id: 'auto-mapped', text: 'Auto-mapped' }]; \ No newline at end of file diff --git a/ui/tests/unit/utilities/constants.test.ts b/ui/tests/unit/utilities/constants.test.ts index 4a77aefc7..ee2c057f0 100644 --- a/ui/tests/unit/utilities/constants.test.ts +++ b/ui/tests/unit/utilities/constants.test.ts @@ -111,7 +111,6 @@ describe('utilities/constants', () => { expect(CONTENT_MAPPING_STATUS['1']).toBe('Mapped'); expect(CONTENT_MAPPING_STATUS['2']).toBe('Updated'); expect(CONTENT_MAPPING_STATUS['3']).toBe('Failed'); - expect(CONTENT_MAPPING_STATUS['5']).toBe('Auto-mapped'); expect(CONTENT_MAPPING_STATUS['4']).toBe('All'); }); From d07597a62ac1b66e34575b5d53aeb98d9750746b Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Mon, 27 Apr 2026 11:23:27 +0530 Subject: [PATCH 16/63] chore: update axios package from version 1.15.0 to 1.15.2 across multiple package.json and package-lock.json files --- api/package-lock.json | 8 +- api/package.json | 2 +- package-lock.json | 6 +- package.json | 4 +- ui/package-lock.json | 63 +++++++---- ui/package.json | 3 +- upload-api/package-lock.json | 210 ++++++++++++++++++++++++++++++++--- upload-api/package.json | 5 +- 8 files changed, 254 insertions(+), 47 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 6790d04b2..8a21fd08a 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -17,7 +17,7 @@ "@emnapi/runtime": "1.9.1", "@emnapi/wasi-threads": "1.2.0", "@wordpress/block-serialization-default-parser": "^5.39.0", - "axios": "^1.15.0", + "axios": "^1.15.2", "cheerio": "^1.2.0", "chokidar": "^3.6.0", "cors": "^2.8.5", @@ -5429,9 +5429,9 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", diff --git a/api/package.json b/api/package.json index fab2384d5..64bb34283 100644 --- a/api/package.json +++ b/api/package.json @@ -38,7 +38,7 @@ "@emnapi/runtime": "1.9.1", "@emnapi/wasi-threads": "1.2.0", "@wordpress/block-serialization-default-parser": "^5.39.0", - "axios": "^1.15.0", + "axios": "^1.15.2", "cheerio": "^1.2.0", "chokidar": "^3.6.0", "cors": "^2.8.5", diff --git a/package-lock.json b/package-lock.json index ec240e212..76d3ff4e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -475,9 +475,9 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", diff --git a/package.json b/package.json index a78e68d59..16583d3ff 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "validate-branch-name": "^1.3.0" }, "overrides": { - "axios": ">=1.15.0", + "axios": ">=1.15.2", "nth-check": ">=2.0.1", - "postcss": ">=8.4.31", + "postcss": ">=8.5.10", "serialize-javascript": ">=6.0.2", "@babel/runtime": ">=7.26.10", "lodash": "^4.18.1", diff --git a/ui/package-lock.json b/ui/package-lock.json index c2d1d164e..26eda5fa6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -15,7 +15,7 @@ "@types/react-dom": "^18.2.13", "@types/react-redux": "^7.1.33", "@vitejs/plugin-react-swc": "^4.2.3", - "axios": "^1.15.0", + "axios": "^1.15.2", "final-form": "^4.20.10", "html-react-parser": "^4.2.9", "jwt-decode": "^4.0.0", @@ -29,6 +29,7 @@ "sass": "^1.68.0", "socket.io-client": "^4.7.5", "typescript": "^4.9.5", + "uuid": "^14.0.0", "vite": "^7.3.2", "vite-tsconfig-paths": "^6.1.1" }, @@ -273,6 +274,15 @@ "uuid": "^8.3.2" } }, + "node_modules/@contentstack/json-rte-serializer/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/venus-components": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@contentstack/venus-components/-/venus-components-3.0.4.tgz", @@ -463,6 +473,15 @@ "tiny-warning": "^1.0.3" } }, + "node_modules/@contentstack/venus-components/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", @@ -3193,9 +3212,9 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -4060,13 +4079,10 @@ } }, "node_modules/dompurify": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", - "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", "license": "(MPL-2.0 OR Apache-2.0)", - "engines": { - "node": ">=20" - }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -4875,15 +4891,16 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -6827,9 +6844,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "funding": [ { "type": "opencollective", @@ -6844,6 +6861,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9037,11 +9055,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/velocity-animate": { diff --git a/ui/package.json b/ui/package.json index 17367d847..642ee3549 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,7 +10,7 @@ "@types/react-dom": "^18.2.13", "@types/react-redux": "^7.1.33", "@vitejs/plugin-react-swc": "^4.2.3", - "axios": "^1.15.0", + "axios": "^1.15.2", "final-form": "^4.20.10", "html-react-parser": "^4.2.9", "jwt-decode": "^4.0.0", @@ -24,6 +24,7 @@ "sass": "^1.68.0", "socket.io-client": "^4.7.5", "typescript": "^4.9.5", + "uuid": "^14.0.0", "vite": "^7.3.2", "vite-tsconfig-paths": "^6.1.1" }, diff --git a/upload-api/package-lock.json b/upload-api/package-lock.json index ac574aa9f..b12a243c3 100644 --- a/upload-api/package-lock.json +++ b/upload-api/package-lock.json @@ -14,7 +14,7 @@ "@typescript-eslint/parser": "^8.56.1", "@wordpress/block-library": "^9.39.0", "@wordpress/blocks": "^15.12.0", - "axios": "^1.15.0", + "axios": "^1.15.2", "chalk": "^4.1.2", "cheerio": "^1.2.0", "cors": "^2.8.5", @@ -38,6 +38,7 @@ "mysql2": "^3.16.2", "php-serialize": "^5.1.3", "prettier": "^3.3.3", + "uuid": "^14.0.0", "winston": "^3.7.2", "xml2js": "^0.6.2" }, @@ -71,11 +72,10 @@ "dependencies": { "fs-readdir-recursive": "^1.1.0", "genson-js": "^0.0.8", - "uuid": "^9.0.1" + "uuid": "^14.0.0" }, "devDependencies": { "@types/fs-readdir-recursive": "^1.1.3", - "@types/uuid": "^9.0.8", "nodemon": "^3.1.10", "rimraf": "^6.0.1", "ts-node": "^10.9.2", @@ -1678,6 +1678,19 @@ "version": "1.14.1", "license": "0BSD" }, + "node_modules/@contentstack/cli-utilities/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@contentstack/management": { "version": "1.27.4", "license": "MIT", @@ -4597,11 +4610,6 @@ "version": "1.3.5", "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "dev": true, - "license": "MIT" - }, "node_modules/@types/wordpress__block-library": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/@types/wordpress__block-library/-/wordpress__block-library-2.6.3.tgz", @@ -5329,6 +5337,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/block-editor/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/block-library": { "version": "9.39.0", "resolved": "https://registry.npmjs.org/@wordpress/block-library/-/block-library-9.39.0.tgz", @@ -5585,6 +5606,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/block-library/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/block-serialization-default-parser": { "version": "5.39.0", "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-5.39.0.tgz", @@ -5702,6 +5736,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/blocks/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/commands": { "version": "1.39.0", "resolved": "https://registry.npmjs.org/@wordpress/commands/-/commands-1.39.0.tgz", @@ -5924,6 +5971,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/commands/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/components": { "version": "27.6.0", "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-27.6.0.tgz", @@ -6266,6 +6326,20 @@ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, + "node_modules/@wordpress/components/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/compose": { "version": "7.39.0", "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-7.39.0.tgz", @@ -6414,6 +6488,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/core-data/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/data": { "version": "9.28.0", "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-9.28.0.tgz", @@ -6845,6 +6932,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/dataviews/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/date": { "version": "5.39.0", "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-5.39.0.tgz", @@ -7259,6 +7359,19 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" }, + "node_modules/@wordpress/image-cropper/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/interactivity": { "version": "6.39.0", "resolved": "https://registry.npmjs.org/@wordpress/interactivity/-/interactivity-6.39.0.tgz", @@ -7715,6 +7828,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/patterns/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/preferences": { "version": "4.39.0", "resolved": "https://registry.npmjs.org/@wordpress/preferences/-/preferences-4.39.0.tgz", @@ -7938,6 +8064,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/preferences/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/primitives": { "version": "4.39.0", "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-4.39.0.tgz", @@ -8235,6 +8374,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/reusable-blocks/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/rich-text": { "version": "7.39.0", "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-7.39.0.tgz", @@ -8549,6 +8701,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/server-side-render/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/shortcode": { "version": "4.39.0", "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-4.39.0.tgz", @@ -8790,6 +8955,19 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/@wordpress/upload-media/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@wordpress/url": { "version": "4.39.0", "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-4.39.0.tgz", @@ -9205,9 +9383,9 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -11241,7 +11419,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -16307,14 +16487,16 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "9.0.1", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { diff --git a/upload-api/package.json b/upload-api/package.json index 58745d61b..4018a037f 100644 --- a/upload-api/package.json +++ b/upload-api/package.json @@ -50,7 +50,7 @@ "@typescript-eslint/parser": "^8.56.1", "@wordpress/block-library": "^9.39.0", "@wordpress/blocks": "^15.12.0", - "axios": "^1.15.0", + "axios": "^1.15.2", "chalk": "^4.1.2", "cheerio": "^1.2.0", "cors": "^2.8.5", @@ -74,12 +74,13 @@ "mysql2": "^3.16.2", "php-serialize": "^5.1.3", "prettier": "^3.3.3", + "uuid": "^14.0.0", "winston": "^3.7.2", "xml2js": "^0.6.2" }, "overrides": { "@contentstack/cli-utilities": { - "axios": ">=1.15.0" + "axios": ">=1.15.2" }, "tmp": ">=0.2.4", "minimatch": ">=10.2.3", From 7ac3a8b68f0fc19b468862b85b8077a738961c46 Mon Sep 17 00:00:00 2001 From: Shradha Nahar Date: Wed, 22 Apr 2026 14:46:54 +0530 Subject: [PATCH 17/63] fix(ui): avoid mapper footer overlap with smart field select menu placement --- ui/src/components/ContentMapper/index.scss | 2 +- ui/src/components/ContentMapper/index.tsx | 117 ++++++++++++++++----- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/ui/src/components/ContentMapper/index.scss b/ui/src/components/ContentMapper/index.scss index 09a207a19..17bc09b96 100644 --- a/ui/src/components/ContentMapper/index.scss +++ b/ui/src/components/ContentMapper/index.scss @@ -487,6 +487,6 @@ div .table-row { .select { .tippy-wrapper { - display: flex; + display: inline; } } \ No newline at end of file diff --git a/ui/src/components/ContentMapper/index.tsx b/ui/src/components/ContentMapper/index.tsx index 99d73fb11..4ab8aacee 100644 --- a/ui/src/components/ContentMapper/index.tsx +++ b/ui/src/components/ContentMapper/index.tsx @@ -1,12 +1,13 @@ // Libraries import { + useCallback, useEffect, useState, useRef, useImperativeHandle, forwardRef, - type ComponentProps, } from 'react'; +import { flushSync } from 'react-dom'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; import { @@ -66,6 +67,7 @@ import { FieldHistoryObj } from './contentMapper.interface'; import { ItemStatusMapProp } from '@contentstack/venus-components/build/components/Table/types'; +import type { ISelectProps } from '@contentstack/venus-components/build/components/Select/Select.d'; import { ModalObj } from '../Modal/modal.interface'; import { UpdatedSettings } from '../AdvancePropertise/advanceProperties.interface'; @@ -85,13 +87,82 @@ import { import './index.scss'; import { NoDataFound, SCHEMA_PREVIEW } from '../../common/assets'; -/** Renders the menu in the document body so `menuPlacement="auto"` matches the control when inside scroll/overflow containers (e.g. InfiniteScrollTable). */ -const CONTENT_MAPPER_SELECT_MENU_PORTAL = - typeof document !== 'undefined' ? document.body : undefined; +const FIELD_MAP_MENU_VIEW_MARGIN = 8; +const FIELD_MAP_MENU_HYSTERESIS = 36; +const FIELD_MAP_MENU_BOTTOM_SLACK = 16; +const FIELD_MAP_MENU_ROW_PX = 34; +const FIELD_MAP_MENU_LIST_CHROME = 28; +const FIELD_MAP_MENU_MIN = 52; +const FIELD_MAP_MENU_MAX_ROWS = 24; +const FIELD_MAP_MENU_CAP = 283; + +function estimateFieldMapMenuHeight( + options: ISelectProps['options'], + maxMenuHeight: number | undefined +): number { + const cap = + typeof maxMenuHeight === 'number' && maxMenuHeight > 0 + ? maxMenuHeight + : FIELD_MAP_MENU_CAP; + const n = Array.isArray(options) ? options.length : 0; + const rows = Math.min(Math.max(n, 1), FIELD_MAP_MENU_MAX_ROWS); + return Math.min(cap, Math.max(FIELD_MAP_MENU_MIN, rows * FIELD_MAP_MENU_ROW_PX + FIELD_MAP_MENU_LIST_CHROME)); +} -const contentMapperSelectMenuStyles = { - menuPortal: (base: Record) => ({ ...base, zIndex: 10001 }), -}; +/** Venus Select for mapping rows: menu opens up when space below (vs `.mapper-footer`) is tight. */ +function FieldMappingSelect(props: ISelectProps) { + const { onMenuOpen, onMenuClose, maxMenuHeight, options, ...rest } = props; + const wrapRef = useRef(null); + const optionsRef = useRef(options); + const maxMenuHeightRef = useRef(maxMenuHeight); + optionsRef.current = options; + maxMenuHeightRef.current = maxMenuHeight; + const [menuPlacement, setMenuPlacement] = useState<'top' | 'bottom'>('bottom'); + + const handleMenuOpen = useCallback(() => { + const el = wrapRef.current; + if (!el) { + onMenuOpen?.(); + return; + } + const rect = el.getBoundingClientRect(); + let spaceBelow = window.innerHeight - rect.bottom - FIELD_MAP_MENU_VIEW_MARGIN; + const footer = document.querySelector('.mapper-footer'); + if (footer instanceof HTMLElement) { + spaceBelow = Math.min( + spaceBelow, + Math.max(0, footer.getBoundingClientRect().top - rect.bottom - FIELD_MAP_MENU_VIEW_MARGIN) + ); + } + const spaceAbove = rect.top - FIELD_MAP_MENU_VIEW_MARGIN; + const need = estimateFieldMapMenuHeight(optionsRef.current, maxMenuHeightRef.current); + const openTop = + spaceBelow + FIELD_MAP_MENU_BOTTOM_SLACK < need && + spaceAbove >= need && + spaceAbove > spaceBelow + FIELD_MAP_MENU_HYSTERESIS; + + flushSync(() => setMenuPlacement(openTop ? 'top' : 'bottom')); + onMenuOpen?.(); + }, [onMenuOpen]); + + const handleMenuClose = useCallback(() => { + setMenuPlacement('bottom'); + onMenuClose?.(); + }, [onMenuClose]); + + return ( +
+ handleValueChange(selectedOption, data?.uid, data?.contentstackFieldUid)} - placeholder="Select Field" - version={'v2'} - maxWidth="290px" - isClearable={false} - options={option} - menuPlacement="auto" - menuPortalTarget={CONTENT_MAPPER_SELECT_MENU_PORTAL} - styles={contentMapperSelectMenuStyles} - isDisabled={ + handleValueChange(selectedOption, data?.uid, data?.contentstackFieldUid)} + placeholder="Select Field" + version={'v2'} + maxWidth="290px" + isClearable={false} + options={option} + isDisabled={ !(data?.contentstackFieldType === 'single_line_text' || data?.contentstackFieldType === 'multi_line_text' || data?.contentstackFieldType === 'html' || data?.contentstackFieldType === 'json') || data?.otherCmsType === undefined || @@ -2423,10 +2491,8 @@ const ContentMapper = forwardRef(({ handleStepChange }: contentMapperProps, ref: position="top" disabled={!selectValueIsExistingField} > - Date: Mon, 27 Apr 2026 23:43:09 +0530 Subject: [PATCH 19/63] feat: enhance WordPress schema mapping with single-child group handling and jetpack/story support - Added `unwrapSingleChildGroup` function to simplify processing of core/group blocks with a single inner block. - Updated `createSchema` to utilize the new function for better handling of nested structures. - Enhanced `schemaMapper` to support jetpack/story blocks, mapping them to a repeatable group with title, alt, caption, and image fields. - Improved unit tests to validate new functionality for single-child groups and jetpack/story mappings. --- api/src/services/wordpress.service.ts | 325 +++++++++++++++--- .../migration-wordpress/libs/schemaMapper.ts | 167 ++++++++- upload-api/src/validators/drupal/index.ts | 21 +- .../migration-wordpress/schemaMapper.test.ts | 51 ++- 4 files changed, 502 insertions(+), 62 deletions(-) diff --git a/api/src/services/wordpress.service.ts b/api/src/services/wordpress.service.ts index e8f6debeb..f04365905 100644 --- a/api/src/services/wordpress.service.ts +++ b/api/src/services/wordpress.service.ts @@ -155,6 +155,19 @@ const resolvedBlockName = (block: any) => { return block?.blockName; }; +/** WordPress core/group with one inner block is not an extra schema level (matches upload-api schemaMapper). */ +function unwrapSingleChildGroup(block: any): any { + let current = block; + while ( + current?.blockName === 'core/group' && + Array.isArray(current?.innerBlocks) && + current.innerBlocks.length === 1 + ) { + current = current.innerBlocks[0]; + } + return current; +} + async function createSchema(fields: any, blockJson : any, title: string, uid: string, assetData: any, duplicateBlockMappings?: Record) { const schema : any = { title: title, @@ -190,7 +203,8 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st // Process each block in blockJson to see if it matches any modular block child for (const block of blockJson) { try { - const blockName = getFieldName(resolvedBlockName(block)); + const blockForProcessing = unwrapSingleChildGroup(block); + const blockName = getFieldName(resolvedBlockName(blockForProcessing)); // Find which modular block child this block matches let matchingChildField = fields.find((childField: any) => { @@ -228,11 +242,15 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st //if (matchingChildField) { // Process innerBlocks (children) if they exist - if (block?.innerBlocks?.length > 0 && Array.isArray(block?.innerBlocks) && matchingModularBlockChild?.uid) { + if (blockForProcessing?.innerBlocks?.length > 0 && Array.isArray(blockForProcessing?.innerBlocks) && matchingModularBlockChild?.uid) { const childrenObject: Record = {}; - block?.innerBlocks?.forEach((child: any, childIndex: number) => { + blockForProcessing.innerBlocks.forEach((child: any, childIndex: number) => { try { + const effectiveChild = unwrapSingleChildGroup(child); + const childBlockName = + getFieldName(resolvedBlockName(effectiveChild))?.toLowerCase() || + getFieldName(resolvedBlockName(effectiveChild)?.toLowerCase()); // Find the field that matches this inner block // Look for fields that belong to this modular_blocks_child const childFieldUid = matchingModularBlockChild?.contentstackFieldUid || getLastUid(matchingModularBlockChild?.contentstackUid); @@ -240,7 +258,6 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st const fUid = f?.contentstackFieldUid || ''; const fOtherCmsType = f?.otherCmsType?.toLowerCase(); const fOtherCmsField = f?.otherCmsField?.toLowerCase(); - const childBlockName = matchingChildField ? matchingChildField?.otherCmsField?.toLowerCase() : (getFieldName(resolvedBlockName(child))?.toLowerCase() || getFieldName(resolvedBlockName(child)?.toLowerCase())); const childKey = getLastUid(f?.contentstackFieldUid); const alreadyPopulated = childrenObject[childKey] !== undefined && childrenObject[childKey] !== null; return fUid.startsWith(childFieldUid + '.') && @@ -253,7 +270,7 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st if (childField?.contentstackFieldType === 'group') { // Process group recursively - handles nested structures - const processedGroup = processNestedGroup(child, childField, fields); + const processedGroup = processNestedGroup(effectiveChild, childField, fields); if (childField?.advanced?.multiple === true && processedGroup) { if (Array.isArray(childrenObject[childKey])) { childrenObject[childKey].push(processedGroup); @@ -263,8 +280,8 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st } else { processedGroup && (childrenObject[childKey] = processedGroup); } - } else { - const formattedChild = formatChildByType(child, childField, assetData); + + const formattedChild = formatChildByType(effectiveChild, childField, assetData); if (childField?.advanced?.multiple === true && formattedChild) { if (Array.isArray(childrenObject[childKey])) { @@ -273,7 +290,6 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st childrenObject[childKey] = [formattedChild]; } } else { - formattedChild && (childrenObject[childKey] = formattedChild); } } @@ -288,12 +304,14 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st modularBlocksArray.push({[getLastUid(matchingModularBlockChild?.contentstackFieldUid)] : childrenObject }); } else if (getLastUid(matchingModularBlockChild?.contentstackFieldUid) && matchingChildField) { // Fallback: inner blocks didn't match child fields (e.g., duplicate-mapped block with different inner block types) - const formattedBlock = formatChildByType(block, matchingChildField, assetData); + + const formattedBlock = formatChildByType(blockForProcessing, matchingChildField, assetData); formattedBlock && modularBlocksArray.push({[getLastUid(matchingModularBlockChild?.contentstackFieldUid)] : { [getLastUid(matchingChildField?.contentstackFieldUid)]: formattedBlock }}); } } else if(getLastUid(matchingModularBlockChild?.contentstackFieldUid) && matchingChildField){ // Handle blocks with no inner blocks - format the block itself - const formattedBlock = formatChildByType(block, matchingChildField, assetData); + + const formattedBlock = formatChildByType(blockForProcessing, matchingChildField, assetData); formattedBlock && modularBlocksArray.push({[getLastUid(matchingModularBlockChild?.contentstackFieldUid)] : { [getLastUid(matchingChildField?.contentstackFieldUid)]: formattedBlock }}); } @@ -319,7 +337,8 @@ async function createSchema(fields: any, blockJson : any, title: string, uid: st // Recursive helper function to process nested group structures function processNestedGroup(child: any, childField: any, allFields: any[]): Record { const nestedChildrenObject: Record = {}; - if (!child?.innerBlocks?.length || !Array.isArray(child?.innerBlocks)) { + const groupBlock = unwrapSingleChildGroup(child); + if (!groupBlock?.innerBlocks?.length || !Array.isArray(groupBlock?.innerBlocks)) { // No nested children, return empty object for group type return {}; } @@ -343,15 +362,28 @@ function processNestedGroup(child: any, childField: any, allFields: any[]): Reco return {}; } - child?.innerBlocks?.forEach((nestedChild: any, nestedIndex: number) => { + groupBlock.innerBlocks.forEach((nestedChild: any, nestedIndex: number) => { try { - - const nestedBlockName = (getFieldName(resolvedBlockName(nestedChild))?.toLowerCase() ?? getFieldName(resolvedBlockName(nestedChild)?.toLowerCase()))?.toLowerCase(); - const nestedChildField = nestedFields?.find((field: any) => - (field?.otherCmsType?.toLowerCase() === nestedBlockName || field?.otherCmsField?.toLowerCase() === nestedBlockName) && !nestedChildrenObject[getLastUid(field?.contentstackFieldUid)]?.length - ); + const nestedEffective = unwrapSingleChildGroup(nestedChild); + const nestedBlockName = + getFieldName(resolvedBlockName(nestedEffective))?.toLowerCase() || + getFieldName(resolvedBlockName(nestedEffective)?.toLowerCase()); + const nestedChildField = nestedFields?.find((field: any) => { + const matchesBlock = + field?.otherCmsType?.toLowerCase() === nestedBlockName || + field?.otherCmsField?.toLowerCase() === nestedBlockName; + + const uid = getLastUid(field?.contentstackFieldUid); + const allowReuse = + field?.advanced?.multiple === true || + !nestedChildrenObject[uid]?.length; + + return matchesBlock && allowReuse; + }); + if (!nestedChildField) { + //console.info("no nested child field found ", nestedChild, nestedChildField, nestedFields, childField) return; } @@ -359,12 +391,13 @@ function processNestedGroup(child: any, childField: any, allFields: any[]): Reco if (nestedChildField?.contentstackFieldType === 'group') { // Recursively process nested groups - const deeplyNestedObject = processNestedGroup(nestedChild, nestedChildField, allFields); - + const deeplyNestedObject = processNestedGroup(nestedEffective, nestedChildField, allFields); if (nestedChildField?.advanced?.multiple === true) { if (Array.isArray(nestedChildrenObject[nestedChildKey])) { + nestedChildrenObject[nestedChildKey].push(deeplyNestedObject); } else { + nestedChildrenObject[nestedChildKey] = [deeplyNestedObject]; } } else { @@ -372,16 +405,35 @@ function processNestedGroup(child: any, childField: any, allFields: any[]): Reco } } else { // Regular field, format it - const formattedNestedChild = formatChildByType(nestedChild, nestedChildField, assetData); - if (nestedChildField?.advanced?.multiple === true) { - if (Array.isArray(nestedChildrenObject[nestedChildKey])) { - nestedChildrenObject[nestedChildKey].push(formattedNestedChild); + // if(nestedChild?.innerBlocks?.length === 1 ){ + // const formattedNestedChild = formatChildByType(nestedChild[0], nestedChildField, assetData); + // if (nestedChildField?.advanced?.multiple === true) { + // if (Array.isArray(nestedChildrenObject[nestedChildKey])) { + + // nestedChildrenObject[nestedChildKey].push(formattedNestedChild); + // } else { + + // nestedChildrenObject[nestedChildKey] = [formattedNestedChild]; + // } + // } else { + // formattedNestedChild && (nestedChildrenObject[nestedChildKey] = formattedNestedChild); + // } + // } + // else { + + const formattedNestedChild = formatChildByType(nestedEffective, nestedChildField, assetData); + if (nestedChildField?.advanced?.multiple === true) { + if (Array.isArray(nestedChildrenObject[nestedChildKey])) { + formattedNestedChild && nestedChildrenObject[nestedChildKey].push(formattedNestedChild); + } else { + formattedNestedChild && (nestedChildrenObject[nestedChildKey] = [formattedNestedChild]); + } } else { - nestedChildrenObject[nestedChildKey] = [formattedNestedChild]; + formattedNestedChild && (nestedChildrenObject[nestedChildKey] = formattedNestedChild); } - } else { - formattedNestedChild && (nestedChildrenObject[nestedChildKey] = formattedNestedChild); - } + + //} + } } catch (nestedError) { console.warn(`Error processing nested child block at index ${nestedIndex}:`, nestedError); @@ -413,6 +465,32 @@ function extractAllHtmlFromInnerBlocks(block: any): any { return html ; } +/** Block HTML: top-level innerHTML, optional attrs/attributes, joined innerContent, or nested innerBlocks. */ +function getBlockInnerHtmlString(block: any): string { + const nonEmpty = (s: unknown): s is string => + typeof s === 'string' && s.trim().length > 0; + + const direct = [ + block?.innerHTML, + block?.attrs?.innerHTML, + block?.attributes?.innerHTML, + block?.innerHtml + ].find(nonEmpty) as string | undefined; + if (direct) { + return direct; + } + if (Array.isArray(block?.innerContent)) { + const fromInnerContent = block.innerContent + .map((p: any) => (typeof p === 'string' ? p : '')) + .join('') + .trim(); + if (fromInnerContent) { + return fromInnerContent; + } + } + return collectHtmlFromInnerBlocks(block); +} + // Helper function to format child blocks based on their type and field configuration function formatChildByType(child: any, field: any, assetData: any) { let formatted ; @@ -425,10 +503,6 @@ function formatChildByType(child: any, field: any, assetData: any) { try { const attrValue = child?.attrs?.innerHTML; - // Check if otherCmsField is "columns" - get all HTML data - if (field?.otherCmsField?.toLowerCase() === 'columns') { - formatted = extractAllHtmlFromInnerBlocks(child); - } // Format based on common field types switch (field?.contentstackFieldType || 'text') { @@ -453,7 +527,12 @@ function formatChildByType(child: any, field: any, assetData: any) { break; case 'json': { - let htmlContent = formatted; + let htmlContent + // Check if otherCmsField is "columns" - get all HTML data + if (field?.otherCmsField?.toLowerCase() === 'columns') { + htmlContent = extractAllHtmlFromInnerBlocks(child); + } + if (!htmlContent && child?.innerBlocks?.length > 0) { htmlContent = collectHtmlFromInnerBlocks(child); } @@ -463,7 +542,10 @@ function formatChildByType(child: any, field: any, assetData: any) { : child; } const hasMeaningfulHtml = stripHtmlTags(htmlContent)?.trim()?.length > 0; - formatted = hasMeaningfulHtml && RteJsonConverter(htmlContent); + // Only set when there is text; do not assign `undefined` (avoids false from `a && fn()` in multi-RTE). + if (hasMeaningfulHtml) { + formatted = RteJsonConverter(htmlContent); + } break; } @@ -471,19 +553,45 @@ function formatChildByType(child: any, field: any, assetData: any) { formatted = child?.blockName ? formatted ?? child?.innerHTML : `

${child}

`; break; - case 'link': - formatted= { - "title": child?.attrs?.service, - "href": child?.attrs?.url - }; + case 'link': { + const attrs = child?.attrs ?? child?.attributes ?? {}; + if (attrs.service) { + formatted = { title: attrs.service, href: attrs.url }; + break; + } + const html = getBlockInnerHtmlString(child); + let href = typeof attrs.url === 'string' && attrs.url ? attrs.url : ''; + let title = ''; + if (html) { + try { + const $ = cheerio.load(html); + const a = $('a').first(); + if (a.length) { + href = a.attr('href') || href; + title = a.text().trim(); + } else { + title = $('button').first().text().trim(); + } + } catch (e) { + console.warn('Error parsing innerHTML for link:', e); + } + } + if (!title) { + title = + (typeof attrs.text === 'string' && attrs.text.trim()) || + (typeof attrs.title === 'string' && attrs.title.trim()) || + (html ? stripHtmlTags(html).trim() : '') || + ''; + } + formatted = { title, href: href || '' }; break; + } case 'file': { - // Extract filename from img tag in innerHTML + // Extract media URL from innerHTML: img (core/image) or audio/source (core/audio) let fileName = ''; let imgUrl = child?.attrs?.src; - - // Check innerHTML for img tag + const innerHtml = child?.innerHTML; if (innerHtml && typeof innerHtml === 'string') { try { @@ -498,13 +606,37 @@ function formatChildByType(child: any, field: any, assetData: any) { const fileNameWithExt = urlParts[urlParts.length - 1].split('?')[0]; // Remove query params fileName = fileNameWithExt.includes('.') ? fileNameWithExt.substring(0, fileNameWithExt.lastIndexOf('.')) : fileNameWithExt; } - + } + if (!fileName) { + const audioTag = $('audio').first(); + let audioSrc = audioTag.attr('src'); + if (!audioSrc) { + audioSrc = audioTag.find('source').first().attr('src') || ''; + } + if (audioSrc) { + imgUrl = audioSrc; + const urlParts = audioSrc.split('/'); + const fileNameWithExt = urlParts[urlParts.length - 1].split('?')[0]; + fileName = fileNameWithExt.includes('.') ? fileNameWithExt.substring(0, fileNameWithExt.lastIndexOf('.')) : fileNameWithExt; + } } } catch (htmlError) { - console.warn('Error parsing innerHTML for img tag:', htmlError); + console.warn('Error parsing innerHTML for img/audio:', htmlError); } } - + // Blocks that store file URL on attrs (e.g. core/file href; some exports typo "herf") + if (!fileName && (child?.attrs?.href || child?.attrs?.herf)) { + const attrHref = child?.attrs?.href || child?.attrs?.herf; + if (typeof attrHref === 'string' && attrHref) { + imgUrl = attrHref; + const urlParts = attrHref.split('/'); + const fileNameWithExt = urlParts[urlParts.length - 1].split('?')[0]; + fileName = fileNameWithExt.includes('.') + ? fileNameWithExt.substring(0, fileNameWithExt.lastIndexOf('.')) + : fileNameWithExt; + } + } + // If no filename extracted from innerHTML, try to get it from src URL if (!fileName && imgUrl) { const urlParts = imgUrl.split('/'); @@ -518,9 +650,40 @@ function formatChildByType(child: any, field: any, assetData: any) { case 'markdown': formatted = stripHtmlTags(child?.innerHTML); break; + + case 'group': { + console.info("child 1 ", child) + const attrs = child?.attrs || child?.attributes; + if ( + field?.advanced?.multiple === true && + child?.blockName === 'jetpack/story' && + Array.isArray(attrs?.mediaFiles) + ) { + formatted = attrs.mediaFiles.map((mf: any) => { + const imgUrl = mf?.url || ''; + let baseName = ''; + if (imgUrl) { + const urlParts = imgUrl.split('/'); + const withExt = urlParts[urlParts.length - 1].split('?')[0]; + baseName = withExt.includes('.') + ? withExt.substring(0, withExt.lastIndexOf('.')) + : withExt; + } + const asset = assetData[baseName?.replace(/-/g, '_')?.toLowerCase()]; + return { + title: mf?.title ?? '', + alt: mf?.alt ?? '', + caption: mf?.caption ?? '', + image: asset, + }; + }); + } + break; + } + default: // Default formatting - preserve original structure with null check - formatted = attrValue; + formatted = attrValue ?? ''; } } catch (attrError) { console.warn(`Error processing attribute ${attrKey}:`, attrError); @@ -532,7 +695,6 @@ function formatChildByType(child: any, field: any, assetData: any) { formatted = 'Failed to process block attributes'; } - return formatted; } const extractCategoryReference = (categories: any) => { @@ -622,6 +784,25 @@ async function saveEntry(fields: any, entry: any, file_path: string, assetData const contentEncoded = $(xmlItem)?.find("content\\:encoded")?.text() || ''; const blocksJson = await setupWordPressBlocks(contentEncoded); + try { + const blocksDir = path.join( + MIGRATION_DATA_CONFIG.DATA, + destinationStackId, + MIGRATION_DATA_CONFIG.WORDPRESS_BLOCKS_DIR_NAME + ); + await fs.promises.mkdir(blocksDir, { recursive: true }); + await writeFileAsync(path.join(blocksDir, `${uid}.json`), blocksJson, 4); + } catch (writeErr) { + customLogger( + project?.id, + destinationStackId, + 'warn', + `Failed to write wordpress blocks JSON for ${uid}: ${ + writeErr instanceof Error ? writeErr.message : String(writeErr) + }` + ); + } + customLogger(project?.id, destinationStackId,'info', `Processed blocks for entry ${uid}`); @@ -1275,11 +1456,20 @@ function isValidImageUrl(url: string): boolean { return true; } +/** True if URL path ends with a common image extension (for image links). */ +function looksLikeImageFileUrl(url: string): boolean { + if (!url || typeof url !== 'string') { + return false; + } + const pathOnly = url.trim().split('?')[0].split('#')[0]; + return /\.(jpe?g|png|gif|webp|svg|bmp|ico|avif|heic|heif)$/i.test(pathOnly); +} + /** - * Extracts image URLs from HTML content + * Extracts image and audio media URLs from HTML content (img, a[href]→image files, audio, CSS backgrounds) * @param htmlContent - The HTML content string * @param baseSiteUrl - Base site URL for resolving relative URLs - * @returns Array of unique image URLs + * @returns Array of unique image and audio URLs */ function extractImageUrlsFromContent(htmlContent: string, baseSiteUrl: string): string[] { if (!htmlContent || typeof htmlContent !== 'string') { @@ -1324,7 +1514,40 @@ function extractImageUrlsFromContent(htmlContent: string, baseSiteUrl: string): }); } }); - + + // Image URLs linked via (skip non-image hrefs) + $('a[href]').each((_, element) => { + const href = $(element).attr('href'); + if (href && isValidImageUrl(href) && looksLikeImageFileUrl(href)) { + const fullUrl = toCheckUrl(href, baseSiteUrl); + if (isValidImageUrl(fullUrl) && looksLikeImageFileUrl(fullUrl)) { + imageUrls.add(fullUrl); + } + } + }); + + // Extract audio src (e.g. core/audio) and nested elements + $('audio').each((_, element) => { + const src = $(element).attr('src'); + if (src && isValidImageUrl(src)) { + const fullUrl = toCheckUrl(src, baseSiteUrl); + if (isValidImageUrl(fullUrl)) { + imageUrls.add(fullUrl); + } + } + $(element) + .find('source') + .each((_, srcEl) => { + const s = $(srcEl).attr('src'); + if (s && isValidImageUrl(s)) { + const fullUrl = toCheckUrl(s, baseSiteUrl); + if (isValidImageUrl(fullUrl)) { + imageUrls.add(fullUrl); + } + } + }); + }); + // Extract background images from style attributes $('[style*="background-image"]').each((_, element) => { const style = $(element).attr('style'); @@ -1358,9 +1581,9 @@ function extractImageUrlsFromContent(htmlContent: string, baseSiteUrl: string): } }); } catch (error) { - console.error('Error extracting image URLs from content:', error); + console.error('Error extracting image/audio URLs from content:', error); } - + return Array.from(imageUrls); } diff --git a/upload-api/migration-wordpress/libs/schemaMapper.ts b/upload-api/migration-wordpress/libs/schemaMapper.ts index 9bb88ee97..518a340d9 100644 --- a/upload-api/migration-wordpress/libs/schemaMapper.ts +++ b/upload-api/migration-wordpress/libs/schemaMapper.ts @@ -238,7 +238,49 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: backupFieldUid: rteUid, advanced: {} }; - }else{ + } + else if (key?.attributes?.originalName === 'jetpack/story') { + const storyGroupUid = rteUid; + const storyFieldBase = fieldName; + const groupSchema: Field[] = [ + { + uid: storyGroupUid, + otherCmsField: getFieldName(resolveBlockName(key)), + otherCmsType: getFieldName(resolveBlockName(key)), + contentstackField: storyFieldBase, + contentstackFieldUid: storyGroupUid, + contentstackFieldType: 'group', + backupFieldType: 'group', + backupFieldUid: storyGroupUid, + advanced: { multiple: true }, + }, + ]; + const storyChildren: Array<{ + key: string; + contentstackFieldType: 'single_line_text' | 'file'; + }> = [ + { key: 'title', contentstackFieldType: 'single_line_text' }, + { key: 'alt', contentstackFieldType: 'single_line_text' }, + { key: 'caption', contentstackFieldType: 'single_line_text' }, + { key: 'image', contentstackFieldType: 'file' }, + ]; + for (const { key: childKey, contentstackFieldType: csType } of storyChildren) { + const childUid = `${storyGroupUid}.${getFieldUid(childKey, affix)}`; + groupSchema.push({ + uid: childUid, + otherCmsField: childKey, + otherCmsType: childKey, + contentstackField: `${storyFieldBase} > ${getFieldName(childKey)}`, + contentstackFieldUid: childUid, + contentstackFieldType: csType, + backupFieldType: csType, + backupFieldUid: childUid, + advanced: {}, + }); + } + return groupSchema; + } + else{ return { uid: rteUid, otherCmsField: getFieldName(resolveBlockName(key)), @@ -250,7 +292,7 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: backupFieldUid: rteUid, advanced: {} }; - } + } case 'core/image': case 'core/audio': case 'core/video': @@ -304,12 +346,80 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: }; } + case 'core/group': { + const inner = key?.innerBlocks; + if (!inner?.length) { + break; + } + + // Single inner block: skip wrapper group uid; inner fields use parentFieldName only + // (no "… > group" segment in labels). + if (inner.length === 1) { + const unwrapped = await processInnerBlocks( + { ...key, innerBlocks: [inner[0]] }, + parentUid, + parentFieldName, + affix + ); + if (!unwrapped?.length) { + break; + } + const flat: Field[] = []; + unwrapped.forEach((schemaObj) => { + if (schemaObj) { + if (Array.isArray(schemaObj)) { + flat.push(...schemaObj); + } else { + flat.push(schemaObj); + } + } + }); + return flat; + } + + const groupSchema: Field[] = []; + const groupUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); + + const innerBlocks = await processInnerBlocks( + key, + groupUid, + fieldName, + affix + ); + innerBlocks?.length > 0 && groupSchema.push({ + uid: groupUid, + otherCmsField: getFieldName(key?.name), + otherCmsType: getFieldName(key?.attributes?.metadata?.name ?? key?.name), + contentstackField: fieldName, + contentstackFieldUid: groupUid, + contentstackFieldType: 'group', + backupFieldType: 'group', + backupFieldUid: groupUid, + advanced: {} + }); + + if (innerBlocks?.length > 0) { + innerBlocks?.forEach((schemaObj) => { + if (schemaObj) { + if (Array.isArray(schemaObj)) { + groupSchema.push(...schemaObj); + } else { + groupSchema.push(schemaObj); + } + } + }); + + return groupSchema; + } + break; + } + + + case 'core/list': case 'core/quote': - case 'core/cover': case 'core/social-links': case 'core/details': - case 'core/group': case 'core/accordion-item': case 'core/accordion-panel': case 'core/navigation': { @@ -351,6 +461,52 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: break; } + + case 'core/cover': + const coverSchema = [] + if(key?.attributes?.url){ + coverSchema.push({ + uid: `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`, + otherCmsField: 'media', + otherCmsType: getFieldName(key?.attributes?.metadata?.name ?? key?.name), + contentstackField: 'media', + contentstackFieldUid: `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`, + contentstackFieldType: 'file', + backupFieldType: 'file', + backupFieldUid: `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`, + advanced: {} + }); + } + + const innerBlocks = await processInnerBlocks( + key, + `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` , + fieldName, + affix + ); + innerBlocks?.length > 0 && coverSchema.push({ + uid: `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`, + otherCmsField: getFieldName(key?.name), + otherCmsType: getFieldName(key?.attributes?.metadata?.name ?? key?.name), + contentstackField: fieldName, + contentstackFieldUid: `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`, + contentstackFieldType: 'group', + backupFieldType: 'group', + backupFieldUid: `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`, + advanced: {} + }); + innerBlocks?.forEach(schemaObj => { + if (schemaObj) { + if (Array.isArray(schemaObj)) { + coverSchema.push(...schemaObj); + } else { + coverSchema.push(schemaObj); + } + } + }); + return coverSchema; + + case 'core/search': { const searchEleUid = parentUid ? `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}` : getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix); @@ -410,7 +566,10 @@ async function schemaMapper (key: WordPressBlock | WordPressBlock[], parentUid: if (innerBlocks?.length === 1) { const items = Array.isArray(innerBlocks[0]) ? innerBlocks[0] : [innerBlocks[0]]; items?.forEach((item: Field) => { + item.uid = `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`; + item.otherCmsField = getFieldName(resolveBlockName(key)); + item.otherCmsType = getFieldName(resolveBlockName(key)); item.contentstackField = `${parentFieldName} > ${getFieldName(resolveBlockName(key))}`; item.contentstackFieldUid = `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`; item.backupFieldUid = `${parentUid}.${getFieldUid(`${key?.name}_${clientIdForUid(key?.clientId)}`, affix)}`; diff --git a/upload-api/src/validators/drupal/index.ts b/upload-api/src/validators/drupal/index.ts index 6983d49ec..9a6327c70 100644 --- a/upload-api/src/validators/drupal/index.ts +++ b/upload-api/src/validators/drupal/index.ts @@ -1,7 +1,18 @@ import mysql from 'mysql2/promise'; -import axios from 'axios'; +import axios, { AxiosHeaders, type AxiosHeaderValue } from 'axios'; import logger from '../../utils/logger'; +function axiosHeaderToString(v: AxiosHeaderValue | undefined): string { + if (v == null) return ''; + if (typeof v === 'string') return v; + if (Array.isArray(v)) return v[0] ?? ''; + if (v instanceof AxiosHeaders) { + return axiosHeaderToString(v.get('content-type') as AxiosHeaderValue); + } + if (typeof v === 'number' || typeof v === 'boolean') return String(v); + return String(v); +} + interface ValidatorProps { data: { host: string; @@ -159,12 +170,14 @@ async function validateAssetsConfig( if (response.status === 200) { // ✅ CHECK CONTENT-TYPE: Ensure it's an actual asset, not an HTML page - const contentType = response.headers['content-type'] || ''; + const contentType = axiosHeaderToString( + (response.headers as AxiosHeaders).get('content-type') + ); // Valid asset content types (not HTML) const isValidAsset = - contentType.includes('image/') || // Images: image/jpeg, image/png, etc. - contentType.includes('application/pdf') || // PDFs + contentType?.includes('image/') || // Images: image/jpeg, image/png, etc. + contentType?.includes('application/pdf') || // PDFs contentType.includes('application/zip') || // Archives contentType.includes('video/') || // Videos contentType.includes('audio/') || // Audio diff --git a/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts b/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts index 1c91d0b86..f44cab33b 100644 --- a/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts +++ b/upload-api/tests/unit/migration-wordpress/schemaMapper.test.ts @@ -241,14 +241,26 @@ describe('schemaMapper', () => { }); describe('group blocks (core/group, core/list, etc.)', () => { - it('returns group wrapper + inner block fields when inner blocks exist', async () => { + it('unwraps single-child core/group (no wrapper row; parent uid only)', async () => { const innerParagraph = makeBlock({ name: 'core/paragraph', clientId: 'p1' }); const block = makeBlock({ name: 'core/group', clientId: 'g1', innerBlocks: [innerParagraph] }); const result = await schemaMapper(block, null, null, affix); expect(Array.isArray(result)).toBe(true); - expect(result[0].contentstackFieldType).toBe('group'); - expect(result.length).toBeGreaterThan(1); + expect(result.every((f: any) => f.contentstackFieldType !== 'group')).toBe(true); + expect(result[0].contentstackFieldType).toBe('json'); + expect(result[0].uid).not.toContain('group_'); + }); + + it('single-child core/group inherits parentUid (no group segment in uid)', async () => { + const innerParagraph = makeBlock({ name: 'core/paragraph', clientId: 'p1' }); + const block = makeBlock({ name: 'core/group', clientId: 'g1', innerBlocks: [innerParagraph] }); + const result = await schemaMapper(block, 'panel_uid', 'Panel', affix); + + expect(Array.isArray(result)).toBe(true); + const p = result[0] as any; + expect(p.uid).toMatch(/^panel_uid\.paragraph_/); + expect(p.contentstackField).toBe('Panel > paragraph'); }); it('marks duplicate inner blocks as multiple', async () => { @@ -323,4 +335,37 @@ describe('schemaMapper', () => { expect(result.contentstackField).toBe('intro_text'); }); }); + + describe('jetpack/story (core/missing)', () => { + it('maps to a repeatable group with title, alt, caption, and image', async () => { + const block = makeBlock({ + name: 'core/missing', + clientId: '63bf87d2-bc77-4517-a491-e30e3e39646d', + attributes: { + originalName: 'jetpack/story', + mediaFiles: [ + { + id: 31, + title: 'image2', + url: 'https://example.com/wp-content/uploads/2025/08/image2.jpeg', + alt: '', + caption: '', + }, + ], + }, + }); + const result = await schemaMapper(block, 'modular_blocks.mb_uid', 'Modular Blocks > story', affix); + expect(Array.isArray(result)).toBe(true); + const group = result.find((f: any) => f.contentstackFieldType === 'group'); + expect(group).toMatchObject({ + contentstackFieldType: 'group', + advanced: { multiple: true }, + otherCmsField: 'story', + }); + const textFields = result.filter((f: any) => f.contentstackFieldType === 'single_line_text'); + expect(textFields.map((f: any) => f.otherCmsField).sort()).toEqual(['alt', 'caption', 'title']); + const imageField = result.find((f: any) => f.otherCmsField === 'image'); + expect(imageField?.contentstackFieldType).toBe('file'); + }); + }); }); From 6f7a40859aa7d3776e6d9fda4777bdfa41a3fd23 Mon Sep 17 00:00:00 2001 From: shobhit-cstk Date: Tue, 21 Apr 2026 13:00:58 +0530 Subject: [PATCH 20/63] feat(contentful): taxonomy migration, locale resolution, tests and config - Extract and map Contentful taxonomies from export; upload-api and mapper integration - Contentful service: field/widget helpers, taxonomy metadata locale resolution (mapper + sys.locale) - API: Vitest thresholds; migration and user unit tests (SSO, createTaxonomy mocks) - app.json placeholder updates and related fixes --- api/src/services/contentful.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/services/contentful.service.ts b/api/src/services/contentful.service.ts index cd94ffb4c..4ecfdda1c 100644 --- a/api/src/services/contentful.service.ts +++ b/api/src/services/contentful.service.ts @@ -215,6 +215,7 @@ function inferContentfulDefaultWidgetId(fieldType: string | undefined): string | return undefined; } } + function getContentfulFieldFromPackage( contentTypesFromPackage: any[] | undefined, ctId: string, @@ -223,6 +224,7 @@ function getContentfulFieldFromPackage( const ct = contentTypesFromPackage?.find((c: any) => c?.sys?.id === ctId); return ct?.fields?.find((f: any) => f?.id === fieldId); } + /** * Picks one fieldMapping row when several share the same `uid` (e.g. bootstrap `title`/`url` rows * from createInitialMapper plus the real Contentful field). Mapper `otherCmsType` is Contentful From cb5d867f20798cc4c7578d1977a23d698a024c03 Mon Sep 17 00:00:00 2001 From: shobhit-cstk Date: Tue, 21 Apr 2026 17:37:20 +0530 Subject: [PATCH 21/63] feat(auth): implement OAuth token handling with HTML responses and organization validation - Updated saveOAuthToken to render HTML success and error pages upon OAuth token exchange. - Introduced organization validation to ensure the correct organization is linked during authorization. - Normalized Contentstack OAuth URLs to avoid issues with hash-style URLs. - Added utility functions for building HTML responses and escaping text for safety. - Enhanced the Login component to handle SSO success and error notifications. - Added unit tests for new utility functions and updated existing tests for auth controller. --- api/src/controllers/auth.controller.ts | 30 ++- api/src/routes/auth.routes.ts | 6 +- api/src/services/auth.service.ts | 39 +++- api/src/utils/contentstack-oauth-url.utils.ts | 17 ++ api/src/utils/oauth-callback-html.utils.ts | 147 +++++++++++++++ api/sso.utils.js | 5 +- .../unit/controllers/auth.controller.test.ts | 33 +++- .../contentstack-oauth-url.utils.test.ts | 20 ++ .../utils/oauth-callback-html.utils.test.ts | 33 ++++ ui/src/pages/Login/index.scss | 20 ++ ui/src/pages/Login/index.tsx | 174 ++++++++++++++---- ui/src/utilities/functions.ts | 14 +- 12 files changed, 489 insertions(+), 49 deletions(-) create mode 100644 api/src/utils/contentstack-oauth-url.utils.ts create mode 100644 api/src/utils/oauth-callback-html.utils.ts create mode 100644 api/tests/unit/utils/contentstack-oauth-url.utils.test.ts create mode 100644 api/tests/unit/utils/oauth-callback-html.utils.test.ts diff --git a/api/src/controllers/auth.controller.ts b/api/src/controllers/auth.controller.ts index ef0d5e61b..d1d0ab2c8 100644 --- a/api/src/controllers/auth.controller.ts +++ b/api/src/controllers/auth.controller.ts @@ -1,6 +1,17 @@ import { Request, Response } from "express"; import { authService } from "../services/auth.service.js"; import { HTTP_CODES } from "../constants/index.js"; +import { + buildOAuthErrorPage, + buildOAuthSuccessPage, +} from "../utils/oauth-callback-html.utils.js"; + +/** Public URL of the Migration Tool UI (Vite default :3000). Override with MIGRATION_UI_ORIGIN in env. */ +const migrationUiOrigin = (): string => { + const raw = process.env.MIGRATION_UI_ORIGIN?.trim(); + if (raw) return raw.replace(/\/$/, ""); + return "http://localhost:3000"; +}; /** * Handles the login request. @@ -42,11 +53,24 @@ const RequestSms = async (req: Request, res: Response) => { /** * Generates the OAuth token and saves it to the database. * @param req - The request object. Sends the code and region. - * @param res - The response object. Sends the message "Token received successfully." + * @param res - Renders an HTML success page (browser OAuth redirect) or HTML error page on failure. */ const saveOAuthToken = async (req: Request, res: Response) => { - await authService.saveOAuthToken(req); - res.status(HTTP_CODES.OK).json({ message: "Token received successfully." }); + const dashboardUrl = `${migrationUiOrigin()}/projects`; + + try { + await authService.saveOAuthToken(req); + + const html = buildOAuthSuccessPage({ dashboardUrl }); + res.status(HTTP_CODES.OK).type("html").send(html); + } catch (error: any) { + const statusCode = + typeof error?.statusCode === "number" ? error.statusCode : HTTP_CODES.SERVER_ERROR; + const message = + error?.message || "Failed to process OAuth callback."; + const html = buildOAuthErrorPage(message, dashboardUrl); + res.status(statusCode).type("html").send(html); + } }; diff --git a/api/src/routes/auth.routes.ts b/api/src/routes/auth.routes.ts index fec39be37..c1b531674 100644 --- a/api/src/routes/auth.routes.ts +++ b/api/src/routes/auth.routes.ts @@ -41,10 +41,8 @@ router.post( ); /** - * Generates the OAuth token and saves it to the database. - * @param req - The request object. Sends the code and region. - * @param res - The response object. Sends the message "Token received successfully." - * @route POST /v2/auth/save-token + * OAuth redirect_uri: exchanges code, saves tokens, responds with HTML success page (or HTML error page). + * @route GET /save-token */ router.get( "/save-token", diff --git a/api/src/services/auth.service.ts b/api/src/services/auth.service.ts index 3349bc60f..da394d6b1 100644 --- a/api/src/services/auth.service.ts +++ b/api/src/services/auth.service.ts @@ -6,6 +6,7 @@ import { LoginServiceType, AppTokenPayload, RefreshTokenResponse } from "../mode import { HTTP_CODES, HTTP_TEXTS, CSAUTHHOST, regionalApiHosts } from "../constants/index.js"; import { generateToken } from "../utils/jwt.utils.js"; import { + AppError, BadRequestError, InternalServerError, ExceptionFunction, @@ -15,8 +16,9 @@ import logger from "../utils/logger.js"; import path from "path"; import fs from "fs"; import axios from "axios"; -import { getAppOrganizationUID } from "../utils/auth.utils.js"; +import { getAppOrganization, getAppOrganizationUID } from "../utils/auth.utils.js"; import { decryptAppConfig } from "../utils/crypto.utils.js"; +import { normalizeContentstackAuthorizeUrl } from "../utils/contentstack-oauth-url.utils.js"; /** * Logs in a user with the provided request data. (No changes needed here) @@ -278,6 +280,24 @@ const saveOAuthToken = async (req: Request): Promise => { const { access_token, refresh_token, organization_uid } = tokenResponse.data; + const expectedOrgUid = getAppOrganizationUID(); + if (!organization_uid) { + throw new BadRequestError( + "No organization was linked to this authorization. When you install or authorize the app in Contentstack, choose the organization that matches your Migration Tool SSO setup, then try again." + ); + } + if (organization_uid !== expectedOrgUid) { + let orgLabel = expectedOrgUid; + try { + orgLabel = getAppOrganization().name; + } catch { + /* keep UID if app.json incomplete */ + } + throw new BadRequestError( + `Organization mismatch: authorize this app in Contentstack for "${orgLabel}" (the organization from your SSO setup). You signed in under a different organization—select the correct one and try SSO again.` + ); + } + const apiHost = regionalApiHosts[region as keyof typeof regionalApiHosts]; const [userErr, userRes] = await safePromise( https({ @@ -332,6 +352,9 @@ const saveOAuthToken = async (req: Request): Promise => { } } catch (error) { + if (error instanceof AppError) { + throw error; + } logger.error("An error occurred during token exchange and save:", error); throw new InternalServerError("Failed to process OAuth callback."); } @@ -430,6 +453,10 @@ export const getAppData = async () => { throw new Error('SSO is not configured. Please run the setup script first.'); } + if (typeof appConfig?.authUrl === "string" && appConfig.authUrl.includes("/#!/apps/")) { + appConfig.authUrl = normalizeContentstackAuthorizeUrl(appConfig.authUrl); + } + return appConfig; } catch (error: any) { @@ -475,9 +502,17 @@ export const checkSSOAuthStatus = async (userId: string) => { const appOrgUID = getAppOrganizationUID(); if (userRecord.organization_uid !== appOrgUID) { + let detail = + 'Organization mismatch: the authorized org does not match the Migration Tool SSO configuration.'; + try { + const { name } = getAppOrganization(); + detail = `Organization mismatch: authorize "${name}" in Contentstack (same org as SSO setup), then try again.`; + } catch { + /* use generic message */ + } return { authenticated: false, - message: 'Organization mismatch' + message: detail, }; } diff --git a/api/src/utils/contentstack-oauth-url.utils.ts b/api/src/utils/contentstack-oauth-url.utils.ts new file mode 100644 index 000000000..bf9679a8b --- /dev/null +++ b/api/src/utils/contentstack-oauth-url.utils.ts @@ -0,0 +1,17 @@ +/** + * Contentstack OAuth docs use path-style authorize URLs, e.g.: + * `{BASE_URL}/apps/{app_uid}/authorize?response_type=code&...` + * + * Hash-style URLs (`{BASE_URL}/#!/apps/{uid}/authorize?...`) are fragile when the user + * is not logged in: the login redirect often drops the hash fragment, so after sign-in + * Contentstack opens the default dashboard (e.g. stacks) instead of resuming authorization + * and org selection. Normalizing to `/apps/.../authorize` preserves the full path across login. + * + * @see https://www.contentstack.com/docs/developers/developer-hub/contentstack-oauth/ + */ +export function normalizeContentstackAuthorizeUrl(authUrl: string): string { + if (!authUrl || typeof authUrl !== "string") { + return authUrl; + } + return authUrl.replace(/\/#!\/apps\//, "/apps/"); +} diff --git a/api/src/utils/oauth-callback-html.utils.ts b/api/src/utils/oauth-callback-html.utils.ts new file mode 100644 index 000000000..fcfbc5cae --- /dev/null +++ b/api/src/utils/oauth-callback-html.utils.ts @@ -0,0 +1,147 @@ +/** + * Escapes text for safe insertion into HTML body text nodes. + */ +export function escapeHtml(text: string): string { + return String(text) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +export type OAuthSuccessPageParams = { + dashboardUrl: string; +}; + +/** + * HTML shown in the browser after OAuth `redirect_uri` completes token exchange. + * Attempts to close SSO popup windows; otherwise redirects to the Migration Tool dashboard. + */ +export function buildOAuthSuccessPage(params: OAuthSuccessPageParams): string { + const { dashboardUrl } = params; + const dashHref = escapeHtml(dashboardUrl); + return ` + + + + + Successfully Authorized + + + + + + +`; +} + +/** Must match `SSO_OAUTH_POSTMESSAGE_SOURCE` in ui `Login/index.tsx` (OAuth callback notify opener). */ +const OAUTH_CALLBACK_POSTMESSAGE_SOURCE = 'cs-migration-oauth-callback'; + +export function buildOAuthErrorPage(message: string, dashboardUrl: string): string { + const safe = escapeHtml(message); + const dashHref = escapeHtml(dashboardUrl); + const messageJs = JSON.stringify(message); + const sourceJs = JSON.stringify(OAUTH_CALLBACK_POSTMESSAGE_SOURCE); + return ` + + + + + Authorization Failed + + + +
+

Something Went Wrong

+

${safe}

+

Back to Migration Tool

+
+ + +`; +} diff --git a/api/sso.utils.js b/api/sso.utils.js index 10fb34522..c80bcc081 100644 --- a/api/sso.utils.js +++ b/api/sso.utils.js @@ -324,8 +324,9 @@ module.exports = async ({ ?.replace(/\//g, "_") ?.replace(/=+$/, ""); - // Generates the authorization URL for the app - const authUrl = `${regionConfig.app}/#!/apps/${ + // Path-style /apps/.../authorize (see Contentstack OAuth docs). Avoids #! hash URLs, + // which are often lost on login redirect so users land on the stacks home instead of org authorize. + const authUrl = `${regionConfig.app}/apps/${ existingApp?.uid }/authorize?response_type=code&client_id=${ oauthData?.client_id diff --git a/api/tests/unit/controllers/auth.controller.test.ts b/api/tests/unit/controllers/auth.controller.test.ts index 2849eb870..cd2bd32fc 100644 --- a/api/tests/unit/controllers/auth.controller.test.ts +++ b/api/tests/unit/controllers/auth.controller.test.ts @@ -1,14 +1,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -const { mockLogin, mockRequestSms } = vi.hoisted(() => ({ +const { mockLogin, mockRequestSms, mockSaveOAuthToken } = vi.hoisted(() => ({ mockLogin: vi.fn(), mockRequestSms: vi.fn(), + mockSaveOAuthToken: vi.fn(), })); vi.mock('../../../src/services/auth.service.js', () => ({ authService: { login: mockLogin, requestSms: mockRequestSms, + saveOAuthToken: mockSaveOAuthToken, }, })); @@ -24,6 +26,8 @@ describe('auth.controller', () => { res = { status: vi.fn().mockReturnThis(), json: vi.fn().mockReturnThis(), + type: vi.fn().mockReturnThis(), + send: vi.fn().mockReturnThis(), }; }); @@ -81,4 +85,31 @@ describe('auth.controller', () => { expect(res.status).toHaveBeenCalledWith(500); }); }); + + describe('saveOAuthToken', () => { + it('should send HTML success page when service resolves', async () => { + mockSaveOAuthToken.mockResolvedValue(undefined); + req.query = { region: 'NA' }; + await authController.saveOAuthToken(req, res); + expect(mockSaveOAuthToken).toHaveBeenCalledWith(req); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.type).toHaveBeenCalledWith('html'); + expect(res.send).toHaveBeenCalled(); + const body = (res.send as ReturnType).mock.calls[0][0] as string; + expect(body).toContain('Successfully Authorized!'); + }); + + it('should send HTML error page when service throws', async () => { + mockSaveOAuthToken.mockRejectedValue({ + statusCode: 400, + message: 'Missing code', + }); + req.query = { region: 'NA' }; + await authController.saveOAuthToken(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(res.type).toHaveBeenCalledWith('html'); + const body = (res.send as ReturnType).mock.calls[0][0] as string; + expect(body).toContain('Missing code'); + }); + }); }); diff --git a/api/tests/unit/utils/contentstack-oauth-url.utils.test.ts b/api/tests/unit/utils/contentstack-oauth-url.utils.test.ts new file mode 100644 index 000000000..1a5637481 --- /dev/null +++ b/api/tests/unit/utils/contentstack-oauth-url.utils.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "vitest"; +import { normalizeContentstackAuthorizeUrl } from "../../../src/utils/contentstack-oauth-url.utils.js"; + +describe("contentstack-oauth-url.utils", () => { + it("rewrites hash SPA authorize URL to path-style", () => { + expect( + normalizeContentstackAuthorizeUrl( + "https://app.contentstack.com/#!/apps/appUid123/authorize?response_type=code&client_id=c" + ) + ).toBe( + "https://app.contentstack.com/apps/appUid123/authorize?response_type=code&client_id=c" + ); + }); + + it("leaves path-style URLs unchanged", () => { + const u = + "https://eu-app.contentstack.com/apps/x/authorize?response_type=code&client_id=c"; + expect(normalizeContentstackAuthorizeUrl(u)).toBe(u); + }); +}); diff --git a/api/tests/unit/utils/oauth-callback-html.utils.test.ts b/api/tests/unit/utils/oauth-callback-html.utils.test.ts new file mode 100644 index 000000000..79d8e22a2 --- /dev/null +++ b/api/tests/unit/utils/oauth-callback-html.utils.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { + escapeHtml, + buildOAuthSuccessPage, + buildOAuthErrorPage, +} from "../../../src/utils/oauth-callback-html.utils.js"; + +describe("oauth-callback-html.utils", () => { + it("escapeHtml escapes special characters", () => { + expect(escapeHtml("