diff --git a/package-lock.json b/package-lock.json index a4f5b6bddf..c5aab3e121 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,25 +16,22 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@terrestris/base-util": "^1.1.0", - "@terrestris/ol-util": "^14.0.0", + "@terrestris/ol-util": "^16.0.0", "@terrestris/react-util": "^4.0.0-beta.3", - "@types/geojson": "^7946.0.12", - "@types/lodash": "^4.14.200", - "ag-grid-community": "^28.2.1", - "ag-grid-react": "^28.2.1", + "ag-grid-community": "^31.1.1", + "ag-grid-react": "^31.1.1", "jspdf": "^2.5.1", "lodash": "^4.17.21", "moment": "^2.29.4", - "proj4": "^2.9.2", - "prop-types": "^15.8.1" + "proj4": "^2.9.2" }, "devDependencies": { - "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.2", + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", "@babel/eslint-parser": "^7.22.15", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/preset-env": "^7.23.2", + "@babel/preset-env": "^7.24.0", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", "@cfaester/enzyme-adapter-react-18": "^0.7.1", @@ -44,18 +41,19 @@ "@semantic-release/git": "^10.0.1", "@terrestris/eslint-config-typescript": "^5.0.0", "@terrestris/eslint-config-typescript-react": "^2.0.0", - "@terrestris/ol-util": "^15.0.0", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/enzyme": "^3.10.15", + "@types/geojson": "^7946.0.12", "@types/jest": "^29.5.6", - "@types/node": "^20.8.9", - "@types/react": "^18.2.33", + "@types/lodash": "^4.14.200", + "@types/node": "^20.11.24", + "@types/react": "^18.2.63", "@types/react-dom": "^18.2.14", - "@typescript-eslint/eslint-plugin": "^7.0.1", - "@typescript-eslint/parser": "^7.0.1", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", "antd": "^5.10.2", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", @@ -68,12 +66,12 @@ "coveralls": "^3.1.1", "css-loader": "^6.8.1", "enzyme": "^3.11.0", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-plugin-jest-dom": "^5.1.0", "eslint-plugin-markdown": "^3.0.1", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-simple-import-sort": "^12.0.0", "eslint-plugin-testing-library": "^6.1.0", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^9.0.0", @@ -96,7 +94,7 @@ "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.3.3", "use-resize-observer": "^9.1.0", - "webpack": "^5.89.0", + "webpack": "^5.90.3", "whatwg-fetch": "^3.6.19" }, "engines": { @@ -104,10 +102,10 @@ "npm": ">=9" }, "peerDependencies": { - "@terrestris/ol-util": ">=15", + "@terrestris/ol-util": ">=16", "@types/react": ">=18", "antd": "^5", - "ol": ">=8", + "ol": ">=9", "ol-mapbox-style": ">=12", "react": ">=18", "react-dom": ">=18" @@ -263,9 +261,9 @@ } }, "node_modules/@babel/core": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -273,11 +271,11 @@ "@babel/generator": "^7.23.6", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.9", - "@babel/parser": "^7.23.9", - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -511,9 +509,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -631,14 +629,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", "dev": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -659,9 +657,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1478,14 +1476,14 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.0.tgz", + "integrity": "sha512-y/yKMm7buHpFFXfxVFS4Vk1ToRJDilIa6fKRioB9Vjichv58TDGXTvqV0dN7plobAmTW5eSEGXDngE+Mm+uO+w==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.23.3" }, @@ -1863,14 +1861,14 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.9.tgz", - "integrity": "sha512-3kBGTNBBk9DQiPoXYS0g0BYlwTQYUTifqgKTjxUwEUkduRT2QOa0FPGBJ+NROQhGyYO5BuTJwGvBnqKDykac6A==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", "dev": true, "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-option": "^7.23.5", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", @@ -1923,7 +1921,7 @@ "@babel/plugin-transform-new-target": "^7.23.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", "@babel/plugin-transform-object-super": "^7.23.3", "@babel/plugin-transform-optional-catch-binding": "^7.23.4", "@babel/plugin-transform-optional-chaining": "^7.23.4", @@ -2027,23 +2025,23 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", @@ -2052,8 +2050,8 @@ "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2062,9 +2060,9 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2862,9 +2860,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5380,9 +5378,9 @@ } }, "node_modules/@terrestris/ol-util": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@terrestris/ol-util/-/ol-util-15.0.0.tgz", - "integrity": "sha512-FgLQzpqQvz7t93ncRYXe6bUVGy7Z6mPoFvXgTSK409aaOVY3c0qeXE5A96NusqWgnuTiGZPfsjbldCXMjwOovg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@terrestris/ol-util/-/ol-util-16.0.0.tgz", + "integrity": "sha512-CJeO0XZrZFjFW7tibfyDLjHtxDxXGLj1coZoEydaqVuEvpR3aYMOiC1/fLrQuXzXX/RwvLPjGEcvexNVHQX+5g==", "dependencies": { "@mapbox/node-pre-gyp": "^1.0.11", "@terrestris/base-util": "^1.0.1", @@ -7429,7 +7427,8 @@ "node_modules/@types/geojson": { "version": "7946.0.14", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", - "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true }, "node_modules/@types/glob": { "version": "7.2.0", @@ -7590,9 +7589,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -7644,9 +7643,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.55", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz", - "integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==", + "version": "18.2.63", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.63.tgz", + "integrity": "sha512-ppaqODhs15PYL2nGUOaOu2RSCCB4Difu4UFrP4I3NHLloXC/ESQzQMi9nvjfT1+rudd0d2L3fQPJxRSey+rGlQ==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -7796,16 +7795,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", - "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", + "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/type-utils": "7.0.2", - "@typescript-eslint/utils": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.1", + "@typescript-eslint/type-utils": "7.1.1", + "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/visitor-keys": "7.1.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -7864,15 +7863,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", - "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", + "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.1", + "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/visitor-keys": "7.1.1", "debug": "^4.3.4" }, "engines": { @@ -7892,13 +7891,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", - "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", + "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2" + "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/visitor-keys": "7.1.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -7909,13 +7908,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", - "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", + "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/typescript-estree": "7.1.1", + "@typescript-eslint/utils": "7.1.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -7936,9 +7935,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", - "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", + "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -7949,13 +7948,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", - "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", + "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/visitor-keys": "7.1.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -8010,17 +8009,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", + "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.1", + "@typescript-eslint/types": "7.1.1", + "@typescript-eslint/typescript-estree": "7.1.1", "semver": "^7.5.4" }, "engines": { @@ -8068,12 +8067,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", - "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", + "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/types": "7.1.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -8361,19 +8360,19 @@ } }, "node_modules/ag-grid-community": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-28.2.1.tgz", - "integrity": "sha512-DMZh/xD/FqYP17qJ1M92PolTYe+hrKuEaf+A4h13O6qn2x/xZQrTRGW5DgnQLR/uLMe1XXZQPKR3UKgAlKo69A==" + "version": "31.1.1", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.1.1.tgz", + "integrity": "sha512-tiQZ7VQ07yJScTMIQpaYoUMPgiyXMwYDcwTxe4riRrcYGTg0e258XEihoPUZFejR60P1fYWMxdJaR2JUnyhGrg==" }, "node_modules/ag-grid-react": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-28.2.1.tgz", - "integrity": "sha512-3vbw+B77uWwAyiOJxQA5U+PQFRCCccUx7L5PIwrnA4Y7c1yAu8sB65hAZdBc9kuW26iltwv7asq0UzP7UAQUpg==", + "version": "31.1.1", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-31.1.1.tgz", + "integrity": "sha512-aaDMSP8MGhoXL5M9c4UmhBClRlc3mEMMC0E0/1mhXU6bdiz0QxXT/xQtDe3DFC62VrtXVda9x20Lpj6p6Bfy8g==", "dependencies": { + "ag-grid-community": "31.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { - "ag-grid-community": "~28.2.1", "react": "^16.3.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0" } @@ -8773,6 +8772,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz", + "integrity": "sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -8809,6 +8827,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, "node_modules/array.prototype.tosorted": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz", @@ -13499,16 +13529,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -13593,27 +13623,29 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.34.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.0.tgz", + "integrity": "sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.7", + "array.prototype.findlast": "^1.2.4", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.3", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.17", "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.7", + "object.fromentries": "^2.0.7", + "object.hasown": "^1.1.3", + "object.values": "^1.1.7", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.10" }, "engines": { "node": ">=4" @@ -13686,9 +13718,9 @@ } }, "node_modules/eslint-plugin-simple-import-sort": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz", - "integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.0.0.tgz", + "integrity": "sha512-8o0dVEdAkYap0Cn5kNeklaKcT1nUsa3LITWEuFk3nJifOoD+5JQGoyDUW2W/iPWwBsNBJpyJS9y4je/BgxLcyQ==", "dev": true, "peerDependencies": { "eslint": ">=5.0.0" diff --git a/package.json b/package.json index 819283a035..0750269853 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "lint:styleguide": "eslint -c .eslintrc.styleguide.js src/", "lint:styleguide-fix": "eslint -c .eslintrc.styleguide.js src/ --fix", "prepare": "husky install", - "start:styleguide": "styleguidist server", "pretest": "npm run typecheck && npm run lint", + "start:styleguide": "styleguidist server", "test": "npm run test:jest", "test:jest": "jest --maxWorkers=4 --coverage", "test:watch": "jest --watchAll", @@ -75,25 +75,22 @@ "@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/react-fontawesome": "^0.2.0", "@terrestris/base-util": "^1.1.0", - "@terrestris/ol-util": "^14.0.0", + "@terrestris/ol-util": "^16.0.0", "@terrestris/react-util": "^4.0.0-beta.3", - "@types/geojson": "^7946.0.12", - "@types/lodash": "^4.14.200", - "ag-grid-community": "^28.2.1", - "ag-grid-react": "^28.2.1", + "ag-grid-community": "^31.1.1", + "ag-grid-react": "^31.1.1", "jspdf": "^2.5.1", "lodash": "^4.17.21", "moment": "^2.29.4", - "proj4": "^2.9.2", - "prop-types": "^15.8.1" + "proj4": "^2.9.2" }, "devDependencies": { - "@babel/cli": "^7.23.0", - "@babel/core": "^7.23.2", + "@babel/cli": "^7.23.9", + "@babel/core": "^7.24.0", "@babel/eslint-parser": "^7.22.15", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/preset-env": "^7.23.2", + "@babel/preset-env": "^7.24.0", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", "@cfaester/enzyme-adapter-react-18": "^0.7.1", @@ -103,18 +100,19 @@ "@semantic-release/git": "^10.0.1", "@terrestris/eslint-config-typescript": "^5.0.0", "@terrestris/eslint-config-typescript-react": "^2.0.0", - "@terrestris/ol-util": "^15.0.0", "@testing-library/dom": "^9.3.3", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/enzyme": "^3.10.15", + "@types/geojson": "^7946.0.12", "@types/jest": "^29.5.6", - "@types/node": "^20.8.9", - "@types/react": "^18.2.33", + "@types/lodash": "^4.14.200", + "@types/node": "^20.11.24", + "@types/react": "^18.2.63", "@types/react-dom": "^18.2.14", - "@typescript-eslint/eslint-plugin": "^7.0.1", - "@typescript-eslint/parser": "^7.0.1", + "@typescript-eslint/eslint-plugin": "^7.1.1", + "@typescript-eslint/parser": "^7.1.1", "antd": "^5.10.2", "babel-jest": "^29.7.0", "babel-loader": "^9.1.3", @@ -127,12 +125,12 @@ "coveralls": "^3.1.1", "css-loader": "^6.8.1", "enzyme": "^3.11.0", - "eslint": "^8.56.0", + "eslint": "^8.57.0", "eslint-plugin-jest-dom": "^5.1.0", "eslint-plugin-markdown": "^3.0.1", - "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react": "^7.34.0", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-simple-import-sort": "^12.0.0", "eslint-plugin-testing-library": "^6.1.0", "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^9.0.0", @@ -155,14 +153,14 @@ "tsconfig-paths-webpack-plugin": "^4.1.0", "typescript": "^5.3.3", "use-resize-observer": "^9.1.0", - "webpack": "^5.89.0", + "webpack": "^5.90.3", "whatwg-fetch": "^3.6.19" }, "peerDependencies": { - "@terrestris/ol-util": ">=15", + "@terrestris/ol-util": ">=16", "@types/react": ">=18", "antd": "^5", - "ol": ">=8", + "ol": ">=9", "ol-mapbox-style": ">=12", "react": ">=18", "react-dom": ">=18" diff --git a/src/CoordinateInfo/CoordinateInfo.example.md b/src/CoordinateInfo/CoordinateInfo.example.md index cb2e3e60ff..ee9ffdaaf1 100644 --- a/src/CoordinateInfo/CoordinateInfo.example.md +++ b/src/CoordinateInfo/CoordinateInfo.example.md @@ -2,6 +2,8 @@ import { faCopy } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import CoordinateInfo from '@terrestris/react-geo/dist/CoordinateInfo/CoordinateInfo'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import { Button, Spin, @@ -9,8 +11,6 @@ import { Tooltip } from 'antd'; import * as copy from 'copy-to-clipboard'; -import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; -import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import { fromLonLat } from 'ol/proj'; @@ -64,88 +64,88 @@ const CoordinateInfoExample = () => { height: '400px' }} /> - { - const features = opts.features; - const clickCoord = opts.clickCoordinate; - const loading = opts.loading; + { + const features = opts.features; + const clickCoord = opts.clickCoordinate; + const loading = opts.loading; - return ( - Object.keys(features).length === 1 && features[Object.keys(features)[0]].length === 1 ? -
-
+
+ - - - - -
-
+ + - - - - -
-
: - Click on a state to get details about the clicked coordinate. - ); - }} - /> - - ); +
+
+ + + + +
+ : + Click on a state to get details about the clicked coordinate. + ); + }} + /> + + ); } diff --git a/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.example.md b/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.example.md index 432137b5ef..b1e72ed748 100644 --- a/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.example.md +++ b/src/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo.example.md @@ -3,9 +3,9 @@ This demonstrates the usage of the CoordinateReferenceSystemCombo. ```jsx import CoordinateReferenceSystemCombo from '@terrestris/react-geo/dist/Field/CoordinateReferenceSystemCombo/CoordinateReferenceSystemCombo'; -import { applyTransform } from 'ol/extent'; import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; +import { applyTransform } from 'ol/extent'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import { @@ -13,11 +13,11 @@ import { get, getTransform } from 'ol/proj'; +import { register } from 'ol/proj/proj4'; import OlSourceOSM from 'ol/source/OSM'; import OlView from 'ol/View'; -import React, { useEffect, useState } from 'react'; import proj4 from 'proj4'; -import { register } from 'ol/proj/proj4'; +import React, { useEffect, useState } from 'react'; const predefinedCrsDefinitions = [{ code: '25832', @@ -46,7 +46,7 @@ const predefinedCrsDefinitions = [{ const CoordinateReferenceSystemComboExample = () => { const [map, setMap] = useState(); - + useEffect(() => { setMap(new OlMap({ layers: [ diff --git a/src/Field/WfsSearchField/WfsSearchField.example.md b/src/Field/WfsSearchField/WfsSearchField.example.md index 89efed03a7..d58aa8464a 100644 --- a/src/Field/WfsSearchField/WfsSearchField.example.md +++ b/src/Field/WfsSearchField/WfsSearchField.example.md @@ -3,15 +3,15 @@ Type a country name in its own languageā€¦ ```jsx import WfsSearch from '@terrestris/react-geo/dist/Field/WfsSearchField/WfsSearchField'; +import AgFeatureGrid from '@terrestris/react-geo/dist/Grid/AgFeatureGrid/AgFeatureGrid'; import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; -import AgFeatureGrid from '@terrestris/react-geo/dist/Grid/AgFeatureGrid/AgFeatureGrid'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import { fromLonLat } from 'ol/proj'; import OlSourceOSM from 'ol/source/OSM'; import OlView from 'ol/View'; -import {useEffect, useState} from 'react'; +import { useEffect, useState } from 'react'; const WfsSearchFieldExample = () => { @@ -31,15 +31,14 @@ const WfsSearchFieldExample = () => { zoom: 4 }) }); - setMap(newMap); }, []); - + if (!map) { return null; } - - const onFeaturesChange = f => setInputFeatures(f); + + const onFeaturesChange = f => setInputFeatures(f); return ( @@ -49,7 +48,7 @@ const WfsSearchFieldExample = () => { placeholder="Type a countryname in its own languageā€¦" baseUrl='https://ows-demo.terrestris.de/geoserver/osm/wfs' featureTypes={['osm:osm-country-borders']} - featureNS={"osm"} + featureNS={'osm'} maxFeatures={3} attributeDetails={{ 'osm:osm-country-borders': { diff --git a/src/Grid/AgFeatureGrid/AgFeatureGrid.example.md b/src/Grid/AgFeatureGrid/AgFeatureGrid.example.md index 984e8537f5..0b9213ba54 100644 --- a/src/Grid/AgFeatureGrid/AgFeatureGrid.example.md +++ b/src/Grid/AgFeatureGrid/AgFeatureGrid.example.md @@ -2,13 +2,14 @@ This example demonstrates the usage of the AgFeatureGrid: ```jsx import AgFeatureGrid from '@terrestris/react-geo/dist/Grid/AgFeatureGrid/AgFeatureGrid'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import OlFormatGeoJSON from 'ol/format/GeoJSON'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; import { fromLonLat } from 'ol/proj'; import OlSourceOSM from 'ol/source/OSM'; import OlView from 'ol/View'; -import * as PropTypes from 'prop-types'; import * as React from 'react'; import federalStates from '../../../assets/federal-states-ger.json'; @@ -16,104 +17,75 @@ import federalStates from '../../../assets/federal-states-ger.json'; const format = new OlFormatGeoJSON(); const features = format.readFeatures(federalStates); -class NameColumnRenderer extends React.Component { - render() { - const { - value - } = this.props; - return {value}; - } -} - -NameColumnRenderer.propTypes = { - value: PropTypes.any -}; - -class MathRoundRenderer extends React.Component { - render() { - const { - value - } = this.props; - return Math.round(value); - } -} - -MathRoundRenderer.propTypes = { - value: PropTypes.any -}; +const AgFeatureGridExample = () => { -class AgFeatureGridExample extends React.Component { + const nameColumnRenderer = cellRendererParams => { + const value = cellRendererParams.value; + return {value}; + }; - constructor(props) { - - super(props); - - this.mapDivId = `map-${Math.random()}`; + const mathRoundRenderer = cellRendererParams => { + const value = cellRendererParams.value; + return ( + <> + {Math.round(value)} + + ) + }; - this.map = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 + const map = new OlMap({ + layers: [ + new OlLayerTile({ + name: 'OSM', + source: new OlSourceOSM() }) - }); - } + ], + view: new OlView({ + center: fromLonLat([37.40570, 8.81566]), + zoom: 4 + }) + }); - componentDidMount() { - this.map.setTarget(this.mapDivId); - } + const colDefs = [{ + cellRenderer: nameColumnRenderer, + field: 'GEN', + filter: true, + headerName: 'Name', + resizable: true, + sortable: true + }, { + cellRenderer: mathRoundRenderer, + field: 'SHAPE_LENG', + filter: true, + headerName: 'Length', + resizable: true, + sortable: true + }, { + cellRenderer: mathRoundRenderer, + field: 'SHAPE_AREA', + filter: true, + headerName: 'Area', + resizable: true, + sortable: true + }]; - render() { - return ( -
- -
-
- ); - } + return ( + + + + + ); } diff --git a/src/Grid/AgFeatureGrid/AgFeatureGrid.spec.tsx b/src/Grid/AgFeatureGrid/AgFeatureGrid.spec.tsx index 7eecb76df0..24d2a2e744 100644 --- a/src/Grid/AgFeatureGrid/AgFeatureGrid.spec.tsx +++ b/src/Grid/AgFeatureGrid/AgFeatureGrid.spec.tsx @@ -1,5 +1,6 @@ -import {CellMouseOverEvent, RowClickedEvent, SelectionChangedEvent} from 'ag-grid-community'; -import _differenceWith from 'lodash/differenceWith'; +import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import _isNil from 'lodash/isNil'; import OlFeature from 'ol/Feature'; import OlGeometry from 'ol/geom/Geometry'; @@ -7,25 +8,30 @@ import OlGeomGeometryCollection from 'ol/geom/GeometryCollection'; import OlLayerVector from 'ol/layer/Vector'; import OlMap from 'ol/Map'; import OlSourceVector from 'ol/source/Vector'; -import { - act -} from 'react-dom/test-utils'; +import OlStyle from 'ol/style/Style'; +import React from 'react'; import TestUtil from '../../Util/TestUtil'; +import { defaultFeatureGridLayerName } from '../commonGrid'; import AgFeatureGrid from './AgFeatureGrid'; describe('', () => { let map: OlMap; let features: OlFeature[]; + const data = [{ + id: 1, + name: 'Shinji Kagawa' + }, { + id: 2, + name: 'Marco Reus' + }, { + id: 3, + name: 'Roman Weidenfeller' + }]; beforeEach(() => { map = TestUtil.createMap(); - features = [ - {id: 1, name: 'Shinji Kagawa'}, - {id: 2, name: 'Marco Reus'}, - {id: 3, name: 'Roman Weidenfeller'} - ].map((prop) => TestUtil.generatePointFeature(prop)); - + features = data.map((prop) => TestUtil.generatePointFeature(prop)); }); afterEach(() => { @@ -43,141 +49,76 @@ describe('', () => { }); it('initializes a vector layer on mount (if map prop is given)', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map}); + const testLayerName = 'my-test-vector-layer'; + let layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === testLayerName); + expect(layerCand).toHaveLength(0); - const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === wrapper.prop('layerName')); + renderInMapContext(map, ( + + )); + + layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === testLayerName); expect(layerCand).toHaveLength(1); expect(layerCand[0]).toBeInstanceOf(OlLayerVector); - const instance1 = wrapper.instance() as AgFeatureGrid; - expect(instance1._source).toBeInstanceOf(OlSourceVector); - expect(instance1._layer).toBeInstanceOf(OlLayerVector); }); - it('initializes a vector layer if it\'s not already added to the map only', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map}); - const instance = wrapper.instance() as AgFeatureGrid; - instance.initVectorLayer(map); + it('sets the given featureStyle to the featurelayer', () => { + const featureStyle = new OlStyle(); + renderInMapContext(map, ( + + )); - const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === wrapper.prop('layerName')); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - expect(layerCand).toHaveLength(1); - expect(layerCand[0]).toBeInstanceOf(OlLayerVector); - expect(instance._source).toBeInstanceOf(OlSourceVector); - expect(instance._layer).toBeInstanceOf(OlLayerVector); - }); - - it('sets the given featureStyle to the featurelayer', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - expect(instance._layer?.getStyle()).toEqual(wrapper.prop('featureStyle')); + expect((layerCand as OlLayerVector)?.getStyle()).toBe(featureStyle); }); it('removes the vector layer from the map on unmount', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map}); - - const layerName = wrapper.prop('layerName'); + const { unmount } = renderInMapContext(map, ( + + )); - wrapper.unmount(); + unmount(); - const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === layerName); + const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === defaultFeatureGridLayerName); expect(layerCand).toHaveLength(0); }); - it('registers a pointermove and singleclick map event handler on mount', () => { - const mapOnSpy = jest.spyOn(map, 'on'); - - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, selectable: true}); - const instance = wrapper.instance() as AgFeatureGrid; - - const onPointerMove = instance.onMapPointerMove; - const onMapSingleClick = instance.onMapSingleClick; - - expect(mapOnSpy).toHaveBeenCalledTimes(2); - expect(mapOnSpy).toHaveBeenCalledWith('pointermove', onPointerMove); - expect(mapOnSpy).toHaveBeenCalledWith('singleclick', onMapSingleClick); - - mapOnSpy.mockRestore(); - }); - - it('unregisters a pointermove and singleclick map event handler on unmount', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, selectable: true}); - const instance = wrapper.instance() as AgFeatureGrid; - - const mapUnSpy = jest.spyOn(map, 'un'); - const onPointerMove = instance.onMapPointerMove; - const onMapSingleClick = instance.onMapSingleClick; + it('renders the given features (in the layer and the grid)', () => { + renderInMapContext(map, ( + + )); - wrapper.unmount(); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - expect(mapUnSpy).toHaveBeenCalledTimes(2); - expect(mapUnSpy).toHaveBeenCalledWith('pointermove', onPointerMove); - expect(mapUnSpy).toHaveBeenCalledWith('singleclick', onMapSingleClick); - - mapUnSpy.mockRestore(); - }); + expect((layerCand as OlLayerVector)?.getSource()?.getFeatures()).toHaveLength(3); - it('generates the column definition out of the given features and takes attributeBlacklist into account', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - const got = instance.getColumnDefs(); - - const exp = [{ - field: 'id', - headerName: 'id' - }, { - field: 'name', - headerName: 'name' - }]; - - expect(got).toEqual(exp); - - wrapper.setProps({ - attributeBlacklist: ['id'] - }); - - const gotBlacklisted = instance.getColumnDefs(); - - const expBlacklisted = [{ - field: 'name', - headerName: 'name' - }]; - - expect(gotBlacklisted).toEqual(expBlacklisted); - }); - - it('generates the appropriate data to render', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - const got = instance.getRowData(); - - const expRows = [{ - id: 1, - name: 'Shinji Kagawa' - }, { - id: 2, - name: 'Marco Reus' - }, { - id: 3, - name: 'Roman Weidenfeller' - }]; - - expRows.forEach((row, idx) => { - let gotElement = got[idx] as any; - expect(row.id).toEqual(gotElement.id); - expect(row.name).toEqual(gotElement.name); + data.forEach( ({ name }) => { + expect(screen.getByText(name)).toBeVisible(); }); }); it('fits the map to show all given features', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - const mapViewFitSpy = jest.spyOn(map.getView(), 'fit'); - - instance.zoomToFeatures(features); + renderInMapContext(map, ( + + )); const featGeometries: OlGeometry[] = []; features.forEach(feature => { @@ -189,186 +130,87 @@ describe('', () => { expect(mapViewFitSpy).toHaveBeenCalledWith(new OlGeomGeometryCollection(featGeometries).getExtent()); }); - it('highlights all given features', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - instance.highlightFeatures(features); - - features.forEach(feature => { - expect(feature.getStyle()).toEqual(wrapper.prop('highlightStyle')); - }); - }); - - it('selects all given features', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - instance.selectFeatures(features); - - features.forEach(feature => { - expect(feature.getStyle()).toEqual(wrapper.prop('selectStyle')); - }); - }); - - it('resets all given features to default feature style', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - instance.resetFeatureStyles(); - - features.forEach(feature => { - expect(feature.getStyle()).toBe(undefined); - }); - }); - - it('returns the feature for a given row key', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features}); - const instance = wrapper.instance() as AgFeatureGrid; - // @ts-ignore - const rowKey = features[1].ol_uid; - - expect(instance.getFeatureFromRowKey(rowKey)).toEqual(features[1]); - }); - - it('selects the feature on row click', () => { - const onRowClickSpy = jest.fn(); - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features, onRowClick: onRowClickSpy}); - const instance = wrapper.instance() as AgFeatureGrid; - const clickedRow = { - data: { - // @ts-ignore - key: features[0].ol_uid - } - } as RowClickedEvent; - const zoomToFeaturesSpy = jest.spyOn(instance, 'zoomToFeatures'); + it('applies the feature select style to the clicked table row', async () => { + const selectStyle = new OlStyle(); - instance.onRowClick(clickedRow); + renderInMapContext(map, ( + + )); - expect(onRowClickSpy).toHaveBeenCalled(); - expect(zoomToFeaturesSpy).not.toHaveBeenCalled(); - onRowClickSpy.mockRestore(); - zoomToFeaturesSpy.mockRestore(); - }); + // TODO: check how can checkbox select be triggered correctly using testing-library + const rows = screen.getAllByRole('presentation'); - it('highlights the feature on row mouse over', () => { - const onRowMouseOverSpy = jest.fn(); - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map, features, onRowMouseOver: onRowMouseOverSpy}); - const clickedRow = { - data: { - // @ts-ignore - key: features[0].ol_uid + for (const row of rows) { + if (row.className.indexOf('ag-checkbox-input') > -1) { + await userEvent.click(row); } - } as CellMouseOverEvent; - const instance = wrapper.instance() as AgFeatureGrid; - const highlightFeaturesSpy = jest.spyOn(instance, 'highlightFeatures'); + } - instance.onRowMouseOver(clickedRow); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - expect(onRowMouseOverSpy).toHaveBeenCalled(); - expect(highlightFeaturesSpy).toHaveBeenCalled(); + const feats = (layerCand as OlLayerVector)?.getSource()?.getFeatures() || []; - onRowMouseOverSpy.mockRestore(); - highlightFeaturesSpy.mockRestore(); + for (const feat of feats) { + expect(feat.getStyle()).toBeDefined(); // TODO + } }); - it('handles the change of props', () => { - const wrapper = TestUtil.mountComponent(AgFeatureGrid, {map: map}); - const instance = wrapper.instance() as AgFeatureGrid; - expect(instance._source).toBeInstanceOf(OlSourceVector); - expect(instance._layer).toBeInstanceOf(OlLayerVector); - - expect(instance._source?.getFeatures()).toEqual([]); - - const zoomToFeaturesSpy = jest.spyOn(instance, 'zoomToFeatures'); - - wrapper.setProps({ - map: map, - features: features, - zoomToExtent: true - }); - - expect(instance._source?.getFeatures()).toEqual(features); - expect(zoomToFeaturesSpy).toHaveBeenCalled(); - - zoomToFeaturesSpy.mockRestore(); - - const mapOnSpy = jest.spyOn(map, 'on'); + it('resets all given features to default feature style', () => { + renderInMapContext(map, ( + + )); - wrapper.setProps({ - selectable: true + features.forEach(feature => { + expect(feature.getStyle()).toBeNull(); }); + }); - expect(mapOnSpy).toHaveBeenCalled(); + it('highlights the feature on row mouse over', async () => { + const hoverStyle = new OlStyle(); - mapOnSpy.mockRestore(); + renderInMapContext(map, ( + + )); - const mapUnSpy = jest.spyOn(map, 'un'); + const row = screen.getByText('Shinji Kagawa'); - wrapper.setProps({ - selectable: false - }); + await userEvent.hover(row); - expect(mapUnSpy).toHaveBeenCalled(); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); + const featCand = (layerCand as OlLayerVector)?.getSource()?.getFeatures() + .find(feat => feat.get('name') === 'Shinji Kagawa'); - mapUnSpy.mockRestore(); + expect(featCand?.getStyle()).toBe(hoverStyle); }); - it('handles row de-selection correctly', () => { - expect.assertions(3); - const onRowSelectionChange = jest.fn(); - const mockedGetSelectedRows = jest.fn(); - const selectionCurrent = [{ - key: '1', - name: 'Yarmolenko' - }, { - key: '2', - name: 'Kagawa' - }, { - key: '3', - name: 'Zorc' - }, { - key: '4', - name: 'Chapuisat' - }]; - - const selectionAfter = [{ - key: '1', - name: 'Yarmolenko' - }, { - key: '2', - name: 'Kagawa' - }]; - - mockedGetSelectedRows.mockReturnValueOnce(selectionAfter); - const wrapper = TestUtil.mountComponent(AgFeatureGrid, { - map, - features, - onRowSelectionChange - }); - act(() => { - wrapper.setState({ - selectedRows: selectionCurrent - }, () => { - const instance = wrapper.instance() as AgFeatureGrid; - const mockedEvt = { - api: { - getSelectedRows: mockedGetSelectedRows - }, - }; - act(() => { - instance.onSelectionChanged(mockedEvt as any as SelectionChangedEvent); - }); - expect(onRowSelectionChange).toHaveBeenCalledTimes(1); - // selectedRows is the first passed parameter - const selectedRows = onRowSelectionChange.mock.calls[0][0]; - expect(selectedRows).toEqual(selectionAfter); - - // deselectedRows is the third passed parameter - const deselectedRows = _differenceWith(selectionCurrent, selectionAfter, (a,b) => a.key === b.key); - const deselectedRowsIs = onRowSelectionChange.mock.calls[0][2]; - expect(deselectedRowsIs).toEqual(deselectedRows); - }); - }); + it('respects the column definition override', async () => { + const columnNameToCheck = 'My nice test column header'; + renderInMapContext(map, ( + + )); + const columnTitle = screen.getByText(columnNameToCheck); + + expect(columnTitle).toBeVisible(); }); }); diff --git a/src/Grid/AgFeatureGrid/AgFeatureGrid.tsx b/src/Grid/AgFeatureGrid/AgFeatureGrid.tsx index 293c7522f8..4bfc5b5d40 100644 --- a/src/Grid/AgFeatureGrid/AgFeatureGrid.tsx +++ b/src/Grid/AgFeatureGrid/AgFeatureGrid.tsx @@ -1,124 +1,77 @@ -/* eslint-disable testing-library/render-result-naming-convention */ import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-theme-balham.css'; import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil'; +import { useOlLayer } from '@terrestris/react-util'; +import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap'; import { CellMouseOutEvent, CellMouseOverEvent, - DetailGridInfo, + GridApi, + GridReadyEvent, + RowClassParams, RowClickedEvent, RowNode, + RowStyle, SelectionChangedEvent } from 'ag-grid-community'; +import { ColDef, ColDefField, ColGroupDef } from 'ag-grid-community/dist/lib/entities/colDef'; import { AgGridReact, AgGridReactProps } from 'ag-grid-react'; import _differenceWith from 'lodash/differenceWith'; -import _isArray from 'lodash/isArray'; -import _isEqual from 'lodash/isEqual'; +import _has from 'lodash/has'; import _isFunction from 'lodash/isFunction'; import _isNil from 'lodash/isNil'; +import _isNumber from 'lodash/isNumber'; +import _isString from 'lodash/isString'; import { getUid } from 'ol'; import OlFeature from 'ol/Feature'; import OlGeometry from 'ol/geom/Geometry'; -import OlGeomGeometryCollection from 'ol/geom/GeometryCollection'; import OlLayerBase from 'ol/layer/Base'; import OlLayerVector from 'ol/layer/Vector'; -import OlMap from 'ol/Map'; import OlMapBrowserEvent from 'ol/MapBrowserEvent'; import OlSourceVector from 'ol/source/Vector'; -import OlStyleCircle from 'ol/style/Circle'; -import OlStyleFill from 'ol/style/Fill'; -import OlStyleStroke from 'ol/style/Stroke'; -import OlStyle from 'ol/style/Style'; -import * as React from 'react'; -import { Key } from 'react'; +import React, { Key, ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; import { CSS_PREFIX } from '../../constants'; +import { + defaultFeatureGridLayerName, + defaultFeatureStyle, + defaultHighlightStyle, + defaultSelectStyle, + highlightFillColor, + RgCommonGridProps +} from '../commonGrid'; + +export type WithKey = { + key: Key; +} & T; -interface OwnProps { +interface OwnProps { /** * The height of the grid. */ - height: number | string; + height?: number | string; /** * The theme to use for the grid. See * https://www.ag-grid.com/javascript-grid-styling/ for available options. * Note: CSS must be loaded to use the theme! */ - theme: string; - /** - * The features to show in the grid and the map (if set). - */ - features: OlFeature[]; - /** - */ - attributeBlacklist: string[]; - /** - * The default style to apply to the features. - */ - featureStyle: OlStyle | (() => OlStyle); - /** - * The highlight style to apply to the features. - */ - highlightStyle: OlStyle | (() => OlStyle); - /** - * The select style to apply to the features. - */ - selectStyle: OlStyle | (() => OlStyle); - /** - * The name of the vector layer presenting the features in the grid. - */ - layerName: string; + theme?: string; /** * Custom column definitions to apply to the given column (mapping via key). */ - columnDefs: any; - /** - * A Function that creates the rowkey from the given feature. - * Receives the feature as property. - * Default is: feature => feature.ol_uid - */ - keyFunction: (feature: OlFeature) => string; - /** - * Whether the map should center on the current feature's extent on init or - * not. - */ - zoomToExtent: boolean; - /** - * Whether rows and features should be selectable or not. - */ - selectable: boolean; + columnDefs?: (ColDef> | ColGroupDef>)[] | null; /** * The width of the grid. */ - width: number | string; - /** - * A CSS class which should be added to the table. - */ - className?: string; - /** - * The map the features should be rendered on. - */ - map: OlMap; + width?: number | string; /** * Custom row data to be shown in feature grid. This might be helpful if * original feature properties should be manipulated in some way before they * are represented in grid. * If provided, #getRowData method won't be called. */ - rowData?: any[]; - /** - * Callback function, that will be called on rowclick. - */ - onRowClick?: (row: any, feature: OlFeature, evt: RowClickedEvent) => void; - /** - * Callback function, that will be called on rowmouseover. - */ - onRowMouseOver?: (row: any, feature: OlFeature, evt: CellMouseOverEvent) => void; - /** - * Callback function, that will be called on rowmouseout. - */ - onRowMouseOut?: (row: any, feature: OlFeature, evt: CellMouseOutEvent) => void; + rowData?: WithKey[]; /** * Callback function, that will be called if the selection changes. */ @@ -131,7 +84,7 @@ interface OwnProps { ) => void; /** * Optional callback function, that will be called if `selectable` is set - * `true` and the a `click` event on the map occurs, e.g. a feature has been + * `true` and the `click` event on the map occurs, e.g. a feature has been * selected in the map. The function receives the olEvt and the selected * features (if any). */ @@ -139,293 +92,132 @@ interface OwnProps { /* * A Function that is called once the grid is ready. */ - onGridIsReady?: (grid: any) => void; + onGridIsReady?: (gridReadyEvent: GridReadyEvent>) => void; + /** + * A custom rowStyle function (if used: row highlighting is overwritten) + */ + rowStyleFn?: (params: RowClassParams>) => RowStyle | undefined; } -interface AgFeatureGridState { - grid: DetailGridInfo | null; - selectedRows: RowNode[]; -} +const defaultClassName = `${CSS_PREFIX}ag-feature-grid`; -export type AgFeatureGridProps = OwnProps & AgGridReactProps; +export type AgFeatureGridProps = OwnProps & RgCommonGridProps & AgGridReactProps; /** * The AgFeatureGrid. - * - * @class The AgFeatureGrid - * @extends React.Component */ -export class AgFeatureGrid extends React.Component { +export function AgFeatureGrid({ + attributeBlacklist = [], + className, + columnDefs, + featureStyle = defaultFeatureStyle, + features = [], + height = 250, + highlightStyle = defaultHighlightStyle, + keyFunction = getUid, + layerName = defaultFeatureGridLayerName, + onGridIsReady = () => undefined, + onMapSingleClick, + onRowClick, + onRowMouseOut, + onRowMouseOver, + onRowSelectionChange, + rowData, + rowStyleFn, + selectStyle = defaultSelectStyle, + selectable = false, + theme = 'ag-theme-balham', + width, + zoomToExtent = false, + ...agGridPassThroughProps +}: AgFeatureGridProps): ReactElement>> | null { /** * The default properties. */ - static defaultProps = { - theme: 'ag-theme-balham', - height: 250, - features: [], - attributeBlacklist: [], - featureStyle: new OlStyle({ - fill: new OlStyleFill({ - color: 'rgba(255, 255, 255, 0.5)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }), - image: new OlStyleCircle({ - radius: 6, - fill: new OlStyleFill({ - color: 'rgba(255, 255, 255, 0.5)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }) - }) - }), - highlightStyle: new OlStyle({ - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }), - image: new OlStyleCircle({ - radius: 6, - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }) - }) - }), - selectStyle: new OlStyle({ - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 2 - }), - image: new OlStyleCircle({ - radius: 6, - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 2 - }) - }) - }), - layerName: 'react-geo-feature-grid-layer', - columnDefs: {}, - keyFunction: getUid, - zoomToExtent: false, - selectable: false - }; - - /** - * The reference of this grid. - * @private - */ - _ref: AgGridReact | null = null; - - /** - * The className added to this component. - * @private - */ - _className = `${CSS_PREFIX}ag-feature-grid`; - /** - * The source holding the features of the grid. - * @private - */ - _source: OlSourceVector | null = null; - - /** - * The layer representing the features of the grid. - * @private - */ - _layer: OlLayerVector | null = null; - - /** - * The constructor. - */ - constructor(props: AgFeatureGridProps) { - super(props); + const [gridApi, setGridApi] = useState> | undefined>(undefined); + const [selectedRows, setSelectedRows] = useState[]>([]); + const [highlightedRows, setHighlightedRows] = useState([]); - this.state = { - grid: null, - selectedRows: [] - }; - } + const map = useMap(); - /** - * Called on lifecycle phase componentDidMount. - */ - componentDidMount() { - const { - map, - features, - zoomToExtent - } = this.props; - - if (!_isNil(map)) { - this.initVectorLayer(map); - this.initMapEventHandlers(map); - if (zoomToExtent) { - this.zoomToFeatures(features); - } - } + const gridVectorLayer = useOlLayer(() => new OlLayerVector({ + properties: { + name: layerName, + }, + source: new OlSourceVector({ + features + }), + style: featureStyle + }), [features, layerName], true); - } + const checkBoxColumnDefinition: ColDef> = useMemo(() => ({ + checkboxSelection: true, + headerCheckboxSelection: true, + headerName: '', + lockPosition: true, + pinned: 'left', + suppressHeaderMenuButton: true, + suppressMovable: true, + width: 40 + }), []); /** - * Invoked immediately after updating occurs. This method is not called for - * the initial render. + * Returns the currently selected row keys. * - * @param prevProps The previous props. + * @return An array with the selected row keys. */ - componentDidUpdate(prevProps: AgFeatureGridProps) { - const { - map, - features, - selectable, - zoomToExtent - } = this.props; - - if (!(_isEqual(prevProps.map, map) && !_isNil(map))) { - this.initVectorLayer(map!); - this.initMapEventHandlers(map!); - } - - if (!(_isEqual(prevProps.features, features))) { - if (this._source) { - this._source.clear(); - this._source.addFeatures(features); - } - - if (zoomToExtent && !_isNil(map)) { - this.zoomToFeatures(features); - } - } - - if (!(_isEqual(prevProps.selectable, selectable))) { - if (selectable && map) { - map.on('singleclick', this.onMapSingleClick); - } else { - if (map) { - map.un('singleclick', this.onMapSingleClick); - } - } + const getSelectedRowKeys = useCallback((): Key[] => { + if (_isNil(gridApi)) { + return []; } - } - /** - * Called on lifecycle phase componentWillUnmount. - */ - componentWillUnmount() { - if (!_isNil(this.props.map)) { - this.deinitVectorLayer(); - this.deinitMapEventHandlers(); - } - } + const sr = gridApi.getSelectedRows(); + return sr.map(row => row.key); + }, [gridApi]); /** - * Initialized the vector layer that will be used to draw the input features - * on and adds it to the map (if any). + * Returns the corresponding rowNode for the given feature id. * - * @param map The map to add the layer to. + * @param key The feature's key to obtain the row from. + * @return he row candidate. */ - initVectorLayer = (map: OlMap) => { - const { - features, - featureStyle, - layerName - } = this.props; - - if (MapUtil.getLayerByName(map, layerName)) { - return; - } - - const source = new OlSourceVector({ - features: features - }); + const getRowFromFeatureKey = useCallback((key: Key): RowNode | undefined => { + let rowNode: RowNode | undefined = undefined; - const layer = new OlLayerVector({ - properties: { - name: layerName, - }, - source: source, - style: featureStyle + gridApi?.forEachNode((node: any) => { + if (node.data.key === key) { + rowNode = node; + } }); - map.addLayer(layer); - - this._source = source; - this._layer = layer; - }; - - /** - * Adds map event callbacks to highlight and select features in the map (if - * given) on pointermove and singleclick. Hovered and selected features will - * be highlighted and selected in the grid as well. - * - * @param map The map to register the handlers to. - */ - initMapEventHandlers = (map: OlMap) => { - const { - selectable - } = this.props; - - map.on('pointermove', this.onMapPointerMove); - - if (selectable) { - map.on('singleclick', this.onMapSingleClick); - } - }; + return rowNode; + }, [gridApi]); /** * Highlights the feature beneath the cursor on the map and in the grid. * * @param olEvt The ol event. */ - onMapPointerMove = (olEvt: any) => { - const { - map, - features, - highlightStyle, - selectStyle - } = this.props; - - const { - grid - } = this.state; - - if (!grid || !grid.api || _isNil(map)) { + const onMapPointerMoveInner = useCallback((olEvt: any) => { + + if (_isNil(gridApi) || _isNil(map)) { return; } - const selectedRowKeys = this.getSelectedRowKeys(); + const selectedRowKeys = getSelectedRowKeys(); - const highlightFeatures = (map.getFeaturesAtPixel(olEvt.pixel, { - layerFilter: layerCand => layerCand === this._layer + const highlightedFeatureArray = (map.getFeaturesAtPixel(olEvt.pixel, { + layerFilter: layerCand => layerCand === gridVectorLayer }) || []) as OlFeature[]; - grid.api?.forEachNode((n) => { - n.setHighlighted(null); - }); + setHighlightedRows([]); features .filter((f): f is OlFeature => !_isNil(f)) .forEach(feature => { - const key = this.props.keyFunction(feature); - + const key = keyFunction(feature); if (selectedRowKeys.includes(key)) { feature.setStyle(selectStyle); } else { @@ -433,39 +225,49 @@ export class AgFeatureGrid extends React.Component !_isNil(f)) .forEach(feat => { - const key = this.props.keyFunction(feat); - grid.api?.forEachNode((n) => { - if (n.data.key === key) { - n.setHighlighted(1); + const key = keyFunction(feat); + gridApi?.forEachNode((n) => { + if (n?.data?.key === key) { + rowsToHighlight.push(n?.data?.key); feat.setStyle(highlightStyle); } }); }); - }; + setHighlightedRows(rowsToHighlight); + }, [gridVectorLayer, features, getSelectedRowKeys, gridApi, highlightStyle, keyFunction, map, selectStyle]); + + const getRowStyle = useCallback((params: RowClassParams>): RowStyle | undefined => { + if (!_isNil(rowStyleFn)) { + return rowStyleFn(params); + } + + if (!_isNil(params?.node?.data?.key) && highlightedRows?.includes(params?.node?.data?.key)) { + return { + backgroundColor: highlightFillColor + }; + } + + return; + }, [highlightedRows, rowStyleFn]); /** * Selects the selected feature in the map and in the grid. * * @param olEvt The ol event. */ - onMapSingleClick = (olEvt: OlMapBrowserEvent) => { - const { - map, - selectStyle, - onMapSingleClick - } = this.props; - + const onMapSingleClickInner = useCallback((olEvt: OlMapBrowserEvent) => { if (_isNil(map)) { return; } - const selectedRowKeys = this.getSelectedRowKeys(); + const selectedRowKeys = getSelectedRowKeys(); const selectedFeatures = (map.getFeaturesAtPixel(olEvt.pixel, { - layerFilter: (layerCand: OlLayerBase) => layerCand === this._layer + layerFilter: (layerCand: OlLayerBase) => layerCand === gridVectorLayer }) || []) as OlFeature[]; if (_isFunction(onMapSingleClick)) { @@ -473,58 +275,24 @@ export class AgFeatureGrid extends React.Component { - const key = this.props.keyFunction(selectedFeature); + const key = keyFunction(selectedFeature); if (selectedRowKeys && selectedRowKeys.includes(key)) { selectedFeature.setStyle(undefined); - const node = this.getRowFromFeatureKey(key); + const node = getRowFromFeatureKey(key); if (node) { node.setSelected(false); } } else { selectedFeature.setStyle(selectStyle); - const node = this.getRowFromFeatureKey(key); + const node = getRowFromFeatureKey(key); if (node) { node.setSelected(true); } } }); - }; - - /** - * Removes the vector layer from the given map (if any). - */ - deinitVectorLayer = () => { - const { - map - } = this.props; - - if (_isNil(this._layer) || _isNil(map)) { - return; - } - - map.removeLayer(this._layer); - }; - - /** - * Unbinds the pointermove and click event handlers from the map (if given). - */ - deinitMapEventHandlers = () => { - const { - map, - selectable - } = this.props; - - if (!_isNil(map)) { - map.un('pointermove', this.onMapPointerMove); - - if (selectable) { - map.un('singleclick', this.onMapSingleClick); - } - } - - }; + }, [gridVectorLayer, getRowFromFeatureKey, getSelectedRowKeys, keyFunction, map, onMapSingleClick, selectStyle]); /** * Returns the column definitions out of the attributes of the first @@ -532,76 +300,61 @@ export class AgFeatureGrid extends React.Component { - const { - attributeBlacklist, - features, - columnDefs, - selectable - } = this.props; - - const columns: any[] = []; + const getColumnDefsFromFeature = useCallback((): ColDef>[] | undefined => { if (features.length < 1) { return; } - + const columns: ColDef>[] = []; + // assumption: all features in array have the same structure const feature = features[0]; - const props = feature.getProperties(); if (selectable) { - columns.push({ - headerName: '', - checkboxSelection: true, - headerCheckboxSelection: true, - width: 28, - pinned: 'left', - lockPosition: true, - suppressMenu: true, - suppressSorting: true, - suppressFilter: true, - suppressResize: true, - suppressMovable: true - }); + // adds select checkbox column + columns.push(checkBoxColumnDefinition); } - let index = -1; - - Object.keys(props).forEach(key => { + const colDefsFromFeature = Object.keys(props).map((key: string): ColDef> | undefined => { if (attributeBlacklist.includes(key)) { return; } + let filter; + if (props[key] instanceof OlGeometry) { return; } - - if (columnDefs[key] && columnDefs[key].index !== undefined) { - index = columnDefs[key].index; - } else { - ++index; + if (_isNumber(props[key])) { + filter = 'agNumberColumnFilter'; + } + if (_isString(props[key])) { + filter = 'agTextColumnFilter'; } - columns[index] = { + + return { + colId: key, + field: key as ColDefField>, + filter, headerName: key, - field: key, - ...columnDefs[key] + minWidth: 50, + resizable: true, + sortable: true }; }); - return columns; - }; + return [ + ...columns, + ...(colDefsFromFeature.filter(c => !_isNil(c)) as ColDef>[]) + ]; + }, [attributeBlacklist, features, selectable, checkBoxColumnDefinition]); /** - * Returns the table row data from all of the given features. + * Returns the table row data from all the given features. * * @return The table data. */ - getRowData = () => { - const { - features - } = this.props; - - return features.map(feature => { + const getRowData = useCallback((): WithKey[] | undefined => { + return features?.map((feature): WithKey => { const properties = feature.getProperties(); const filtered = Object.keys(properties) .filter(key => !(properties[key] instanceof OlGeometry)) @@ -611,11 +364,11 @@ export class AgFeatureGrid extends React.Component; }); - }; + }, [features, keyFunction]); /** * Returns the corresponding feature for the given table row key. @@ -623,79 +376,26 @@ export class AgFeatureGrid extends React.Component => { - const { - features, - keyFunction - } = this.props; - + const getFeatureFromRowKey = (key: Key): OlFeature => { const feature = features.filter(f => keyFunction(f) === key); - return feature[0]; }; /** - * Returns the corresponding rowNode for the given feature id. - * - * @param key The feature's key to obtain the row from. - * @return he row candidate. - */ - getRowFromFeatureKey = (key: string): RowNode | undefined => { - const { - grid - } = this.state; - - let rowNode: RowNode | undefined = undefined; - - if (!grid || !grid.api) { - return; - } - - grid.api.forEachNode((node: any) => { - if (node.data.key === key) { - rowNode = node; - } - }); - - return rowNode; - }; - - /** - * Returns the currently selected row keys. - * - * @return An array with the selected row keys. - */ - getSelectedRowKeys = (): string[] => { - const { - grid - } = this.state; - - if (!grid || !grid.api) { - return []; - } - - const selectedRows = grid.api.getSelectedRows(); - - return selectedRows.map(row => row.key); - }; - - /** - * Called on row click and zooms the the corresponding feature's extent. + * Called on row click and zooms the corresponding feature's extent. * * @param evt The RowClickedEvent. */ - onRowClick = (evt: RowClickedEvent) => { - const { - onRowClick - } = this.props; - + const onRowClickInner = (evt: RowClickedEvent) => { const row = evt.data; - const feature = this.getFeatureFromRowKey(row.key); + const feature = getFeatureFromRowKey(row.key); if (_isFunction(onRowClick)) { onRowClick(row, feature, evt); } else { - this.zoomToFeatures([feature]); + if (!_isNil(map)) { + MapUtil.zoomToFeatures(map,[feature]); + } } }; @@ -705,19 +405,15 @@ export class AgFeatureGrid extends React.Component { - const { - onRowMouseOver - } = this.props; - + const onRowMouseOverInner = (evt: CellMouseOverEvent) => { const row = evt.data; - const feature = this.getFeatureFromRowKey(row.key); + const feature = getFeatureFromRowKey(row.key); if (_isFunction(onRowMouseOver)) { onRowMouseOver(row, feature, evt); } - this.highlightFeatures([feature]); + highlightFeatures([feature]); }; /** @@ -725,75 +421,40 @@ export class AgFeatureGrid extends React.Component { - const { - onRowMouseOut - } = this.props; - + const onRowMouseOutInner = (evt: CellMouseOutEvent) => { const row = evt.data; - const feature = this.getFeatureFromRowKey(row.key); + const feature = getFeatureFromRowKey(row.key); if (_isFunction(onRowMouseOut)) { onRowMouseOut(row, feature, evt); } - this.unhighlightFeatures([feature]); - }; - - /** - * Fits the map's view to the extent of the passed features. - * - * @param features The features to zoom to. - */ - zoomToFeatures = (features: OlFeature[]) => { - const { - map - } = this.props; - - if (_isNil(map)) { - return; - } - - const featGeometries = features - .map(f => f.getGeometry()) - .filter((f): f is OlGeometry => !_isNil(f)); - - if (featGeometries.length > 0) { - const geomCollection = new OlGeomGeometryCollection(featGeometries); - map.getView().fit(geomCollection.getExtent()); - } + unHighlightFeatures([feature]); }; /** * Highlights the given features in the map. * - * @param highlightFeatures The features to highlight. + * @param featureArray The features to highlight. */ - highlightFeatures = (highlightFeatures: OlFeature[]) => { - const { - highlightStyle - } = this.props; - - highlightFeatures + const highlightFeatures = (featureArray: OlFeature[]) => { + featureArray .filter((f): f is OlFeature => !_isNil(f)) .forEach(feature => feature.setStyle(highlightStyle)); }; /** - * Unhighlights the given features in the map. + * Un-highlights the given features in the map. * - * @param unhighlightFeatures The features to unhighlight. + * @param featureArray The features to un-highlight. */ - unhighlightFeatures = (unhighlightFeatures: OlFeature[]) => { - const { - selectStyle - } = this.props; - const selectedRowKeys = this.getSelectedRowKeys(); + const unHighlightFeatures = (featureArray: OlFeature[]) => { + const selectedRowKeys = getSelectedRowKeys(); - unhighlightFeatures + featureArray .filter((f): f is OlFeature => !_isNil(f)) .forEach(feature => { - const key = this.props.keyFunction(feature); + const key = keyFunction(feature); if (selectedRowKeys && selectedRowKeys.includes(key)) { feature.setStyle(selectStyle); } else { @@ -805,14 +466,10 @@ export class AgFeatureGrid extends React.Component[]) => { - const { - selectStyle - } = this.props; - - features.forEach(feature => { + const selectFeatures = (featureArray: OlFeature[]) => { + featureArray.forEach(feature => { if (feature) { feature.setStyle(selectStyle); } @@ -822,138 +479,147 @@ export class AgFeatureGrid extends React.Component { - const { - features - } = this.props; - + const resetFeatureStyles = () => { features.forEach(feature => feature.setStyle(undefined)); }; /** * Called if the selection changes. */ - onSelectionChanged = (evt: SelectionChangedEvent) => { - const { - onRowSelectionChange - } = this.props; - - const { - grid, - selectedRows - } = this.state; - - let selectedRowsAfter: RowNode[]; - if (!grid || !grid.api) { + const onSelectionChanged = (evt: SelectionChangedEvent) => { + let selectedRowsAfter: WithKey[]; + if (_isNil(gridApi)) { selectedRowsAfter = evt.api.getSelectedRows(); } else { - selectedRowsAfter = grid.api.getSelectedRows(); + selectedRowsAfter = gridApi.getSelectedRows(); } const deselectedRows = _differenceWith(selectedRows, - selectedRowsAfter, (a: RowNode, b: RowNode) => a.key === b.key); + selectedRowsAfter, (a, b) => a.key === b.key); const selectedFeatures = selectedRowsAfter.flatMap(row => { - return row.key ? [this.getFeatureFromRowKey(row.key)] : []; + return row.key ? [getFeatureFromRowKey(row.key)] : []; }); const deselectedFeatures = deselectedRows.flatMap(row => { - return row.key ? [this.getFeatureFromRowKey(row.key)] : []; + return row.key ? [getFeatureFromRowKey(row.key)] : []; }); // update state - this.setState({ - selectedRows: selectedRowsAfter - }); + setSelectedRows(selectedRowsAfter); if (_isFunction(onRowSelectionChange)) { onRowSelectionChange(selectedRowsAfter, selectedFeatures, deselectedRows, deselectedFeatures, evt); } - this.resetFeatureStyles(); - this.selectFeatures(selectedFeatures); + resetFeatureStyles(); + selectFeatures(selectedFeatures); }; /** * - * @param grid + * @param gridReadyEvent */ - onGridReady(grid: any) { - this.setState({ - grid - }, this.onVisiblityChange); - - if (this.props.onGridIsReady) { - this.props.onGridIsReady(grid); + const onGridReady = (gridReadyEvent: GridReadyEvent>) => { + if (!_isNil(gridReadyEvent)) { + setGridApi(gridReadyEvent?.api); + onVisibilityChange(); + onGridIsReady(gridReadyEvent); } - } + }; + + const onVisibilityChange = () => gridApi?.sizeColumnsToFit(); /** - * + * Adds map event callbacks to highlight and select features in the map (if + * given) on pointermove and single-click. Hovered and selected features will + * be highlighted and selected in the grid as well. */ - onVisiblityChange() { - if (this.state.grid) { - this.state.grid.api?.sizeColumnsToFit(); + useEffect(() => { + if (!_isNil(map)) { + map.on('pointermove', onMapPointerMoveInner); + + if (selectable) { + map.on('singleclick', onMapSingleClickInner); + } } - } - /** - * The render method. - */ - render() { - const { - className, - height, - width, - theme, - features, - map, - attributeBlacklist, - onRowClick, - onRowMouseOver, - onRowMouseOut, - zoomToExtent, - selectable, - featureStyle, - highlightStyle, - selectStyle, - layerName, - columnDefs, - children, - rowData, - ...passThroughProps - } = this.props; - - const finalClassName = className - ? `${className} ${this._className} ${theme}` - : `${this._className} ${theme}`; - - return ( -
- this._ref = ref} - {...passThroughProps} - > - {children} - -
- ); - } + return () => { + if (!_isNil(map)) { + map.un('pointermove', onMapPointerMoveInner); + + if (selectable) { + map.un('singleclick', onMapSingleClickInner); + } + } + }; + }, [onMapPointerMoveInner, onMapSingleClickInner, map, selectable]); + + useEffect(() => { + if (!_isNil(features) && features.length > 0 && !_isNil(map) && zoomToExtent) { + MapUtil.zoomToFeatures(map,features); + } + }, [features, map, zoomToExtent]); + + // TODO: move to less? + const outerDivStyle = useMemo(() => ({ + height, + width + }), [width, height]); + + const colDefs = useMemo(() => { + if (!_isNil(columnDefs)) { + if (!selectable) { + return columnDefs; + } + // check for checkbox column - if not present => add + const checkboxSelectionPresent = colDefs?. + some((colDef: ColDef>) => _has(colDef, 'checkboxSelection') && !_isNil(colDef.checkboxSelection)); + if (checkboxSelectionPresent) { + return columnDefs; + } + return [ + checkBoxColumnDefinition, + ...columnDefs + ]; + } + return getColumnDefsFromFeature(); + }, [ + checkBoxColumnDefinition, + columnDefs, + getColumnDefsFromFeature, + selectable + ]); + + const passedRowData = useMemo(() => !_isNil(rowData) ? rowData : getRowData(), [ + rowData, + getRowData + ]); + + const finalClassName = className + ? `${className} ${defaultClassName} ${theme}` + : `${defaultClassName} ${theme}`; + + return ( +
+ > + columnDefs={colDefs} + getRowStyle={getRowStyle} + onCellMouseOut={onRowMouseOutInner} + onCellMouseOver={onRowMouseOverInner} + onGridReady={onGridReady} + onRowClicked={onRowClickInner} + onSelectionChanged={onSelectionChanged} + rowData={passedRowData} + rowSelection="multiple" + suppressRowClickSelection + {...agGridPassThroughProps} + /> +
+ ); + } export default AgFeatureGrid; diff --git a/src/Grid/FeatureGrid/FeatureGrid.example.md b/src/Grid/FeatureGrid/FeatureGrid.example.md index 8c560fdf36..5c6b5dd2ca 100644 --- a/src/Grid/FeatureGrid/FeatureGrid.example.md +++ b/src/Grid/FeatureGrid/FeatureGrid.example.md @@ -2,6 +2,8 @@ This example demonstrates the usage of the FeatureGrid: ```jsx import FeatureGrid from '@terrestris/react-geo/dist/Grid/FeatureGrid/FeatureGrid'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import OlFormatGeoJSON from 'ol/format/GeoJSON'; import OlLayerTile from 'ol/layer/Tile'; import OlMap from 'ol/Map'; @@ -17,78 +19,62 @@ const features = format.readFeatures(federalStates); const nameColumnRenderer = val => {val}; -class FeatureGridExample extends React.Component { - - constructor(props) { - - super(props); - - this.mapDivId = `map-${Math.random()}`; - - this.map = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 +const FeatureGridExample = () => { + const map = new OlMap({ + layers: [ + new OlLayerTile({ + name: 'OSM', + source: new OlSourceOSM() }) - }); - } - - componentDidMount() { - this.map.setTarget(this.mapDivId); - } - - render() { - return ( -
- { - const nameA = a.GEN.toUpperCase(); - const nameB = b.GEN.toUpperCase(); - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - - return 0; - }, - defaultSortOrder: 'ascend' - }, - SHAPE_LENG: { - title: 'Length', - render: val => Math.round(val) - }, - SHAPE_AREA: { - title: 'Area', - render: val => Math.round(val) + ], + view: new OlView({ + center: fromLonLat([37.40570, 8.81566]), + zoom: 4 + }) + }); + + return ( + + { + const nameA = a.GEN.toUpperCase(); + const nameB = b.GEN.toUpperCase(); + if (nameA < nameB) { + return -1; } - }} - /> -
-
- ); - } + if (nameA > nameB) { + return 1; + } + + return 0; + }, + defaultSortOrder: 'ascend' + }, { + dataIndex: 'SHAPE_LENG', + title: 'Length', + render: val => Math.round(val) + }, { + dataIndex: 'SHAPE_AREA', + title: 'Area', + render: val => Math.round(val) + }]} + /> + +
+ ); } @@ -99,6 +85,8 @@ An example with a remote feature source. ```jsx import UrlUtil from '@terrestris/base-util/dist/UrlUtil/UrlUtil'; import FeatureGrid from '@terrestris/react-geo/dist/Grid/FeatureGrid/FeatureGrid'; +import MapComponent from '@terrestris/react-util/dist/Components/MapComponent/MapComponent'; +import MapContext from '@terrestris/react-util/dist/Context/MapContext/MapContext'; import Input from 'antd/lib/input'; import OlFormatGeoJSON from 'ol/format/GeoJSON'; import OlLayerTile from 'ol/layer/Tile'; @@ -111,139 +99,121 @@ import OlStyleStroke from 'ol/style/Stroke'; import OlStyle from 'ol/style/Style'; import OlStyleText from 'ol/style/Text'; import OlView from 'ol/View'; -import * as PropTypes from 'prop-types'; -import * as React from 'react'; +import React, { + useCallback, + useEffect, + useRef, + useState} from 'react'; // Credits to Maps Icons Collection https://mapicons.mapsmarker.com. import mapMarker from '../../../assets/bus-map-marker.png'; -class RemoteFeatureGrid extends React.Component { - - constructor(props) { - super(props); - - this.state = { - loading: false, - features: [], - pagination: { - pageSize: 10, - current: 1 - }, - sorter: { - field: 'name', - order: 'ascend' - }, - nameFilterText: '', - filterDropdownOpen: false - }; - } - - componentDidMount() { - this.fetchData(); - } - - fetchData() { - const { - url - } = this.props; - - const { - pagination, - sorter, - nameFilterText - } = this.state; - - const format = new OlFormatGeoJSON(); - - this.setState({ - loading: true - }); - - const queryParams = { - SERVICE: 'WFS', - VERSION: '1.1.0', - REQUEST: 'GetFeature', - TYPENAME: 'osm:osm-busstops', - MAXFEATURES: pagination.pageSize, - STARTINDEX: (pagination.current - 1) * pagination.pageSize, - OUTPUTFORMAT: 'application/json', - CQL_FILTER: 'BBOX(the_geom, 342395,6206125,352395,6216125)' - }; - - const sortDir = sorter.order === 'ascend' ? ' A' : ' D'; - if (sorter.field) { - queryParams.SORTBY = `${sorter.field}${sortDir}`; +const RemoteFeatureGrid = ({ + url +}) => { + + const [loading, setLoading] = useState(false); + const [features, setFeatures] = useState([]); + const [pageSize, setPageSize] = useState(10); + const [currentPage, setCurrentPage] = useState(1); + const [totalFeatures, setTotalFeatures] = useState(); + const [sorter, setSorter] = useState({ + field: 'name', + order: 'ascend' + }); + const [nameFilterText, setNameFilterText] = useState(); + const [filterDropdownOpen, setFilterDropdownOpen] = useState(false); + + const searchInput = useRef(); + + const fetchData = useCallback(async () => { + setLoading(true); + + try { + const queryParams = { + SERVICE: 'WFS', + VERSION: '1.1.0', + REQUEST: 'GetFeature', + TYPENAME: 'osm:osm-busstops', + MAXFEATURES: pageSize, + STARTINDEX: (currentPage - 1) * pageSize, + OUTPUTFORMAT: 'application/json', + CQL_FILTER: 'BBOX(the_geom, 342395,6206125,352395,6216125)' + }; + + const sortDir = sorter.order === 'ascend' ? ' A' : ' D'; + if (sorter.field) { + queryParams.SORTBY = `${sorter.field}${sortDir}`; + } + + if (nameFilterText) { + queryParams.CQL_FILTER += ` AND name like '%${nameFilterText}%'`; + } + + const query = UrlUtil.objectToRequestString(queryParams); + + const response = await fetch(`${url}?${query}`); + + if (!response.ok) { + throw new Error('No successful response while requesting the features'); + } + + const responseJson = await response.json(); + + const format = new OlFormatGeoJSON(); + + const feats = format.readFeatures(responseJson); + + if (feats.length === 0) { + alert('No features found!'); + setNameFilterText('') + return; + } + + setFeatures(feats); + + setTotalFeatures(responseJson.totalFeatures); + } catch (error) { + logger.error(error); + alert('Could not fetch data!'); + } finally { + setLoading(false); } + }, [url, currentPage, pageSize, sorter, nameFilterText]); + + useEffect(() => { + fetchData(); + }, [fetchData]); - if (nameFilterText) { - queryParams.CQL_FILTER += ` AND name like '%${nameFilterText}%'`; + useEffect(() => { + if (searchInput.current && filterDropdownOpen) { + window.setTimeout(() => { + searchInput.current.focus(); + }, 100) } + }, [filterDropdownOpen]); - const query = UrlUtil.objectToRequestString(queryParams); - - fetch(`${url}?${query}`) - .then(response => response.json()) - .then(response => { - this.setState({ - loading: false - }); - - const features = format.readFeatures(response); - - if (features.length === 0) { - alert('No matches found!'); - this.setState({ - nameFilterText: '' - }); - return; - } - - this.setState({ - features: features, - pagination: { - ...pagination, - total: response.totalFeatures - } - }); - }) - .catch(() => { - this.setState({ - loading: false - }); - alert('Could not fetch data!'); - }); - } + const onTableChange = (page, filters, sort) => { + setSorter({ + field: sort.field, + order: sort.order + }); - onTableChange(pagination, filters, sorter) { - this.setState({ - sorter: { - ...this.state.sorter, - ...sorter - }, - pagination: { - ...this.state.pagination, - current: pagination.current - }, - }, () => this.fetchData()); - } + setCurrentPage(page.current); + setPageSize(page.pageSize); + }; - onNameFilterTextChange(evt) { - this.setState({ - nameFilterText: evt.target.value - }); - } + const onNameFilterSearch = (evt) => { + setCurrentPage(1); + setFilterDropdownOpen(false); + setNameFilterText(evt.target.value); + }; - onNameFilterSearch() { - this.setState({ - pagination: { - ...this.state.pagination, - current: 1 - }, - filterDropdownOpen: false - }, () => this.fetchData()); - } + const onFilterDropdownOpenChange = (visible) => { + setFilterDropdownOpen(visible); + }; - getFeatureStyle(feature, color) { + const getFeatureStyle = (feature, color) => { return new OlStyle({ image: new OlStyleIcon(({ anchor: [0.5, 1.1], @@ -265,120 +235,76 @@ class RemoteFeatureGrid extends React.Component { }); } - render() { - const { - map - } = this.props; - - const { - loading, - features, - pagination, - nameFilterText, - filterDropdownOpen, - } = this.state; + return ( + `Total: ${a}` + }} + featureStyle={feat => getFeatureStyle(feat)} + highlightStyle={feat => getFeatureStyle(feat, 'rgb(230, 247, 255)')} + selectStyle={feat => getFeatureStyle(feature, 'rgb(24, 144, 255)')} + onChange={onTableChange} + columns={[{ + dataIndex: 'name', + sorter: true, + sortOrder: sorter.order, + filterDropdown: ( +
+ +
+ ), + filterDropdownOpen: filterDropdownOpen, + onFilterDropdownOpenChange: onFilterDropdownOpenChange + }]} + /> + ); +} - const getFeatureStyle = this.getFeatureStyle; +const RemoteFeatureGridExample = () => { - return ( - + + - this.searchInput = el} - placeholder="Search name" - value={nameFilterText} - onChange={this.onNameFilterTextChange.bind(this)} - onPressEnter={this.onNameFilterSearch.bind(this)} - /> -
- ), - filterDropdownOpen: filterDropdownOpen, - onFilterDropdownOpenChange: visible => { - this.setState({ - filterDropdownOpen: visible - }, () => { - this.searchInput.focus(); - }); - } - } + style={{ + height: '400px' }} /> - ); - } -} - -RemoteFeatureGrid.propTypes = { - map: PropTypes.instanceOf(OlMap), - url: PropTypes.string -}; - -class RemoteFeatureGridExample extends React.Component { - - constructor(props) { - - super(props); - - this.mapDivId = `map-${Math.random()}`; - - this.map = new OlMap({ - layers: [ - new OlLayerTile({ - name: 'OSM', - source: new OlSourceOSM() - }) - ], - view: new OlView({ - center: fromLonLat([37.40570, 8.81566]), - zoom: 4 - }) - }); - } - - componentDidMount() { - this.map.setTarget(this.mapDivId); - } - - render() { - return ( -
- -
-
- ); - } + + ); } diff --git a/src/Grid/FeatureGrid/FeatureGrid.spec.tsx b/src/Grid/FeatureGrid/FeatureGrid.spec.tsx index 92ede432cf..e7fffbeab0 100644 --- a/src/Grid/FeatureGrid/FeatureGrid.spec.tsx +++ b/src/Grid/FeatureGrid/FeatureGrid.spec.tsx @@ -1,15 +1,15 @@ -import {MapBrowserEvent} from 'ol'; +import { renderInMapContext } from '@terrestris/react-util/dist/Util/rtlTestUtils'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import OlFeature from 'ol/Feature'; -import OlGeometry from 'ol/geom/Geometry'; -import OlGeomGeometryCollection from 'ol/geom/GeometryCollection'; import OlLayerVector from 'ol/layer/Vector'; import OlMap from 'ol/Map'; import OlSourceVector from 'ol/source/Vector'; -import { - act -} from 'react-dom/test-utils'; +import OlStyle from 'ol/style/Style'; +import React from 'react'; import TestUtil from '../../Util/TestUtil'; +import { defaultFeatureGridLayerName } from '../commonGrid'; import FeatureGrid from './FeatureGrid'; describe('', () => { @@ -18,12 +18,16 @@ describe('', () => { beforeEach(() => { map = TestUtil.createMap(); - features = [ - {id: 1, name: 'Shinji Kagawa'}, - {id: 2, name: 'Marco Reus'}, - {id: 3, name: 'Roman Weidenfeller'} - ].map((prop) => TestUtil.generatePointFeature(prop)); - + features = [{ + id: 1, + name: 'Shinji Kagawa' + }, { + id: 2, + name: 'Marco Reus' + }, { + id: 3, + name: 'Roman Weidenfeller' + }].map((prop) => TestUtil.generatePointFeature(prop)); }); afterEach(() => { @@ -32,377 +36,174 @@ describe('', () => { }); it('is defined', () => { - expect(FeatureGrid).not.toBeUndefined(); + expect(FeatureGrid).toBeDefined(); }); it('can be rendered', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid); - expect(wrapper).not.toBeUndefined(); - }); - - it('initializes a vector layer on mount (if map prop is given)', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map}); - - const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === wrapper.prop('layerName')); + const { container } = render( + + ); - expect(layerCand).toHaveLength(1); - expect(layerCand[0]).toBeInstanceOf(OlLayerVector); - expect((wrapper.instance() as FeatureGrid)._source).toBeInstanceOf(OlSourceVector); - expect((wrapper.instance() as FeatureGrid)._layer).toBeInstanceOf(OlLayerVector); - - const wrapperWithoutMap = TestUtil.mountComponent(FeatureGrid); - - expect((wrapperWithoutMap.instance() as FeatureGrid)._source).toBeNull(); - expect((wrapperWithoutMap.instance() as FeatureGrid)._layer).toBeNull(); + expect(container).toBeVisible(); }); - it('initializes a vector layer if it\'s not already added to the map only', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map}); + it('initializes a vector layer on mount', () => { + let layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === defaultFeatureGridLayerName); + + expect(layerCand).toHaveLength(0); - (wrapper.instance() as FeatureGrid).initVectorLayer(map); + renderInMapContext(map, ( + + )); - const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === wrapper.prop('layerName')); + layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === defaultFeatureGridLayerName); expect(layerCand).toHaveLength(1); expect(layerCand[0]).toBeInstanceOf(OlLayerVector); - expect((wrapper.instance() as FeatureGrid)._source).toBeInstanceOf(OlSourceVector); - expect((wrapper.instance() as FeatureGrid)._layer).toBeInstanceOf(OlLayerVector); }); it('sets the given featureStyle to the featurelayer', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - expect((wrapper.instance() as FeatureGrid)._layer?.getStyle()).toEqual(wrapper.prop('featureStyle')); - }); - - it('removes the vector layer from the map on unmount', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map}); - - const layerName = wrapper.prop('layerName'); - - wrapper.unmount(); - - const layerCand = map.getLayers().getArray().filter(layer => layer.get('name') === layerName); - - expect(layerCand).toHaveLength(0); - }); - - it('registers a pointermove and singleclick map event handler on mount', () => { - const mapOnSpy = jest.spyOn(map, 'on'); - - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, selectable: true}); - - const onPointerMove = (wrapper.instance() as FeatureGrid).onMapPointerMove; - const onMapSingleClick = (wrapper.instance() as FeatureGrid).onMapSingleClick; - - expect(mapOnSpy).toHaveBeenCalledTimes(2); - expect(mapOnSpy).toHaveBeenCalledWith('pointermove', onPointerMove); - expect(mapOnSpy).toHaveBeenCalledWith('singleclick', onMapSingleClick); - - mapOnSpy.mockRestore(); - }); - - it('unregisters a pointermove and singleclick map event handler on unmount', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, selectable: true}); - - const mapUnSpy = jest.spyOn(map, 'un'); - const onPointerMove = (wrapper.instance() as FeatureGrid).onMapPointerMove; - const onMapSingleClick = (wrapper.instance() as FeatureGrid).onMapSingleClick; - - wrapper.unmount(); - - expect(mapUnSpy).toHaveBeenCalledTimes(2); - expect(mapUnSpy).toHaveBeenCalledWith('pointermove', onPointerMove); - expect(mapUnSpy).toHaveBeenCalledWith('singleclick', onMapSingleClick); - - mapUnSpy.mockRestore(); - }); - - it('generates the column definition out of the given features and takes attributeBlacklist into account', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - - const got = (wrapper.instance() as FeatureGrid).getColumnDefs(); - - const exp = [{ - dataIndex: 'id', - key: 'id', - title: 'id' - }, { - dataIndex: 'name', - key: 'name', - title: 'name' - }]; - - expect(got).toEqual(exp); - - wrapper.setProps({ - attributeBlacklist: ['id'] - }); - - const gotBlacklisted = (wrapper.instance() as FeatureGrid).getColumnDefs(); - - const expBlacklisted = [{ - dataIndex: 'name', - key: 'name', - title: 'name' - }]; + const featureStyle = new OlStyle(); - expect(gotBlacklisted).toEqual(expBlacklisted); - }); - - it('generates the appropriate data to render', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - - const got = (wrapper.instance() as FeatureGrid).getTableData(); + renderInMapContext(map, ( + + )); - const expRows = [{ - id: 1, - name: 'Shinji Kagawa' - }, { - id: 2, - name: 'Marco Reus' - }, { - id: 3, - name: 'Roman Weidenfeller' - }]; + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - expRows.forEach((row, idx) => { - expect(row.id).toEqual(got[idx].id); - expect(row.name).toEqual(got[idx].name); - }); + expect((layerCand as OlLayerVector)?.getStyle()).toBe(featureStyle); }); - it('fits the map to show all given features', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - - const mapViewFitSpy = jest.spyOn(map.getView(), 'fit'); - - (wrapper.instance() as FeatureGrid).zoomToFeatures(features); - - const featGeometries: OlGeometry[] = []; - features.forEach(feature => { - if (feature.getGeometry()) { - featGeometries.push(feature.getGeometry()!); - } - }); - - expect(mapViewFitSpy).toHaveBeenCalledWith(new OlGeomGeometryCollection(featGeometries).getExtent()); - }); + it('removes the vector layer from the map on unmount', () => { + const { unmount } = renderInMapContext(map, ( + + )); - it('highlights all given features', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); + unmount(); - (wrapper.instance() as FeatureGrid).highlightFeatures(features); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - features.forEach(feature => { - expect(feature.getStyle()).toEqual(wrapper.prop('highlightStyle')); - }); + expect(layerCand).toBeUndefined(); }); - it('unhighlight all given features, but takes selection into account', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - // @ts-ignore - const selectedFeatureUid = features[0].ol_uid; - - act(() => { - wrapper.setState({selectedRowKeys: [selectedFeatureUid]}); - }); - act(() => { - (wrapper.instance() as FeatureGrid).unhighlightFeatures(features); - }); - - features.forEach(feature => { - // @ts-ignore - if (feature.ol_uid === selectedFeatureUid) { - expect(feature.getStyle()).toEqual(wrapper.prop('selectStyle')); - } else { - expect(feature.getStyle()).toBe(undefined); - } - }); - }); + it('renders the given features (in the layer and the grid)', () => { + renderInMapContext(map, ( + + )); - it('selects all given features', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - (wrapper.instance() as FeatureGrid).selectFeatures(features); + expect((layerCand as OlLayerVector)?.getSource()?.getFeatures()).toHaveLength(3); - features.forEach(feature => { - expect(feature.getStyle()).toEqual(wrapper.prop('selectStyle')); - }); + expect(screen.getByText('Shinji Kagawa')).toBeVisible(); + expect(screen.getByText('Marco Reus')).toBeVisible(); + expect(screen.getByText('Roman Weidenfeller')).toBeVisible(); }); - it('resets all given features to default feature style', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); + it('respects the attributeBlacklist', () => { + renderInMapContext(map, ( + + )); - (wrapper.instance() as FeatureGrid).resetFeatureStyles(); - - features.forEach(feature => { - expect(feature.getStyle()).toBe(undefined); - }); - }); - - it('sets the appropriate select style to a feature if selection in grid changes', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - // @ts-ignore - const selectedRowKeys = [features[0].ol_uid, features[1].ol_uid]; - - act(() => { - (wrapper.instance() as FeatureGrid).onSelectChange(selectedRowKeys); - }); - - features.forEach(feature => { - // @ts-ignore - if (selectedRowKeys.includes(feature.ol_uid)) { - expect(feature.getStyle()).toEqual(wrapper.prop('selectStyle')); - } else { - expect(feature.getStyle()).toBe(undefined); - } - }); - - expect(wrapper.state('selectedRowKeys')).toEqual(selectedRowKeys); + expect(screen.queryByText('Shinji Kagawa')).not.toBeInTheDocument(); + expect(screen.queryByText('Marco Reus')).not.toBeInTheDocument(); + expect(screen.queryByText('Roman Weidenfeller')).not.toBeInTheDocument(); }); - it('returns the feature for a given row key', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - // @ts-ignore - const rowKey = features[1].ol_uid; + it('applies the feature hover style to the hovered table row', async () => { + const hoverStyle = new OlStyle(); - expect((wrapper.instance() as FeatureGrid).getFeatureFromRowKey(rowKey)).toEqual(features[1]); - }); + renderInMapContext(map, ( + + )); - it('selects the feature on row click', () => { - const onRowClickSpy = jest.fn(); - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features, onRowClick: onRowClickSpy}); - const clickedRow = { - // @ts-ignore - key: features[0].ol_uid - }; - const zoomToFeaturesSpy = jest.spyOn((wrapper.instance() as FeatureGrid), 'zoomToFeatures'); + const row = screen.getByText('Shinji Kagawa'); - (wrapper.instance() as FeatureGrid).onRowClick(clickedRow); + await userEvent.hover(row); - expect(onRowClickSpy).toHaveBeenCalled(); - expect(zoomToFeaturesSpy).toHaveBeenCalled(); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); + const featCand = (layerCand as OlLayerVector)?.getSource()?.getFeatures() + .find(feat => feat.get('name') === 'Shinji Kagawa'); - onRowClickSpy.mockRestore(); - zoomToFeaturesSpy.mockRestore(); + expect(featCand?.getStyle()).toBe(hoverStyle); }); - it('highlights the feature on row mouse over', () => { - const onRowMouseOverSpy = jest.fn(); - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features, onRowMouseOver: onRowMouseOverSpy}); - const clickedRow = { - // @ts-ignore - key: features[0].ol_uid - }; - const highlightFeaturesSpy = jest.spyOn((wrapper.instance() as FeatureGrid), 'highlightFeatures'); - - (wrapper.instance() as FeatureGrid).onRowMouseOver(clickedRow); + it('applies the feature select style to the clicked table row', async () => { + const selectStyle = new OlStyle(); - expect(onRowMouseOverSpy).toHaveBeenCalled(); - expect(highlightFeaturesSpy).toHaveBeenCalled(); + renderInMapContext(map, ( + + )); - onRowMouseOverSpy.mockRestore(); - highlightFeaturesSpy.mockRestore(); - }); + const rows = screen.getAllByRole('checkbox'); + // Ignore the first one ("Select all") + rows.shift(); - it('unhighlights the feature on row mouse out', () => { - const onRowMouseOutSpy = jest.fn(); - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features, onRowMouseOut: onRowMouseOutSpy}); - const clickedRow = { - // @ts-ignore - key: features[0].ol_uid - }; - const unhighlightFeaturesSpy = jest.spyOn((wrapper.instance() as FeatureGrid), 'unhighlightFeatures'); + for (const row of rows) { + await userEvent.click(row); + } - (wrapper.instance() as FeatureGrid).onRowMouseOut(clickedRow); + const layerCand = map.getLayers().getArray().find(layer => layer.get('name') === defaultFeatureGridLayerName); - expect(onRowMouseOutSpy).toHaveBeenCalled(); - expect(unhighlightFeaturesSpy).toHaveBeenCalled(); + const feats = (layerCand as OlLayerVector)?.getSource()?.getFeatures() || []; - onRowMouseOutSpy.mockRestore(); - unhighlightFeaturesSpy.mockRestore(); + for (const feat of feats) { + expect(feat.getStyle()).toBe(selectStyle); + } }); - it('handles the change of props', () => { - const wrapper = TestUtil.mountComponent(FeatureGrid); + it('zooms to the feature of the clicked table row', async () => { + renderInMapContext(map, ( + + )); - expect((wrapper.instance() as FeatureGrid)._source).toBeNull(); - expect((wrapper.instance() as FeatureGrid)._layer).toBeNull(); + const row = screen.getByText('Shinji Kagawa'); - wrapper.setProps({ - map: map - }); + expect(map.getView().getZoom()).toBeCloseTo(17, 0.05); - expect((wrapper.instance() as FeatureGrid)._source).toBeInstanceOf(OlSourceVector); - expect((wrapper.instance() as FeatureGrid)._layer).toBeInstanceOf(OlLayerVector); + await userEvent.click(row); - expect((wrapper.instance() as FeatureGrid)._source?.getFeatures()).toEqual([]); - - const zoomToFeaturesSpy = jest.spyOn((wrapper.instance() as FeatureGrid), 'zoomToFeatures'); - - wrapper.setProps({ - features: features, - zoomToExtent: true - }); - - expect((wrapper.instance() as FeatureGrid)._source?.getFeatures()).toEqual(features); - expect(zoomToFeaturesSpy).toHaveBeenCalled(); - - zoomToFeaturesSpy.mockRestore(); - - const mapOnSpy = jest.spyOn(map, 'on'); - - wrapper.setProps({ - selectable: true - }); - - expect(mapOnSpy).toHaveBeenCalled(); - - mapOnSpy.mockRestore(); - - const mapUnSpy = jest.spyOn(map, 'un'); - - wrapper.setProps({ - selectable: false - }); - - expect(mapUnSpy).toHaveBeenCalled(); - expect(wrapper.state('selectedRowKeys')).toEqual([]); - - mapUnSpy.mockRestore(); + expect(map.getView().getZoom()).toBeCloseTo(28); }); - it('sets the highlight style to any hovered feature', () => { - const getFeaturesAtPixelSpy = jest.spyOn(map, 'getFeaturesAtPixel') - .mockImplementation(() => [features[0]]); + it('respects the column definition override', async () => { + renderInMapContext(map, ( + + )); - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); + const columnTitle = screen.getByText('Name override'); - (wrapper.instance() as FeatureGrid).onMapPointerMove({ - pixel: [19, 19] - } as unknown as MapBrowserEvent); - - expect(features[0].getStyle()).toEqual(wrapper.prop('highlightStyle')); - - expect(features[1].getStyle()).toEqual(undefined); - expect(features[2].getStyle()).toEqual(undefined); - - getFeaturesAtPixelSpy.mockRestore(); + expect(columnTitle).toBeVisible(); }); - - it('sets the select style to any clicked/selected feature', () => { - const getFeaturesAtPixelSpy = jest.spyOn(map, 'getFeaturesAtPixel') - .mockImplementation(() => [features[0]]); - - const wrapper = TestUtil.mountComponent(FeatureGrid, {map, features}); - - (wrapper.instance() as FeatureGrid).onMapSingleClick({ - pixel: [19, 19] - } as unknown as MapBrowserEvent); - - expect(features[0].getStyle()).toEqual(wrapper.prop('selectStyle')); - - getFeaturesAtPixelSpy.mockRestore(); - }); - }); + diff --git a/src/Grid/FeatureGrid/FeatureGrid.tsx b/src/Grid/FeatureGrid/FeatureGrid.tsx index 62528fe067..02d732b554 100644 --- a/src/Grid/FeatureGrid/FeatureGrid.tsx +++ b/src/Grid/FeatureGrid/FeatureGrid.tsx @@ -1,7 +1,8 @@ -import MapUtil from '@terrestris/ol-util/dist/MapUtil/MapUtil'; +import useMap from '@terrestris/react-util/dist/Hooks/useMap/useMap'; +import useOlLayer from '@terrestris/react-util/dist/Hooks/useOlLayer/useOlLayer'; import { Table } from 'antd'; -import { ColumnProps, TableProps } from 'antd/lib/table'; -import _isEqual from 'lodash/isEqual'; +import { AnyObject } from 'antd/lib/_util/type'; +import { ColumnsType, ColumnType, TableProps } from 'antd/lib/table'; import _isFunction from 'lodash/isFunction'; import _isNil from 'lodash/isNil'; import _kebabCase from 'lodash/kebabCase'; @@ -11,387 +12,119 @@ import OlGeometry from 'ol/geom/Geometry'; import OlGeometryCollection from 'ol/geom/GeometryCollection'; import OlLayerBase from 'ol/layer/Base'; import OlLayerVector from 'ol/layer/Vector'; -import OlMap from 'ol/Map'; import OlMapBrowserEvent from 'ol/MapBrowserEvent'; import RenderFeature from 'ol/render/Feature'; import OlSourceVector from 'ol/source/Vector'; -import OlStyleCircle from 'ol/style/Circle'; -import OlStyleFill from 'ol/style/Fill'; -import OlStyleStroke from 'ol/style/Stroke'; -import OlStyle from 'ol/style/Style'; -import * as React from 'react'; -import { Key } from 'react'; - -interface OwnProps { - /** - * The features to show in the grid and the map (if set). - */ - features: OlFeature[]; - /** - */ - attributeBlacklist: string[]; - /** - * The default style to apply to the features. - */ - featureStyle: OlStyle | (() => OlStyle); - /** - * The highlight style to apply to the features. - */ - highlightStyle: OlStyle | (() => OlStyle); - /** - * The select style to apply to the features. - */ - selectStyle: OlStyle | (() => OlStyle); - /** - * The name of the vector layer presenting the features in the grid. - */ - layerName: string; - /** - * Custom column definitions to apply to the given column (mapping via key). - * See https://ant.design/components/table/#Column. - */ - columnDefs: ColumnProps; - /** - * A Function that creates the rowkey from the given feature. - * Receives the feature as property. - * Default is: feature => feature.ol_uid - * - */ - keyFunction: (feature: OlFeature) => string; - /** - * Whether the map should center on the current feature's extent on init or - * not. - */ - zoomToExtent: boolean; - /** - * Whether rows and features should be selectable or not. - */ - selectable: boolean; - /** - * A CSS class which should be added to the table. - */ - className?: string; - /** - * A CSS class to add to each table row or a function that - * is evaluated for each record. - */ - rowClassName?: string | ((record: any) => string); - /** - * The map the features should be rendered on. If not given, the features - * will be rendered in the table only. - */ - map?: OlMap; - /** - * Callback function, that will be called on rowclick. - */ - onRowClick?: (row: any, feature: OlFeature) => void; - /** - * Callback function, that will be called on rowmouseover. - */ - onRowMouseOver?: (row: any, feature: OlFeature) => void; - /** - * Callback function, that will be called on rowmouseout. - */ - onRowMouseOut?: (row: any, feature: OlFeature) => void; - /** - * Callback function, that will be called if the selection changes. - */ +import React, { Key, useCallback, useEffect, useState } from 'react'; + +import { CSS_PREFIX } from '../../constants'; +import { + defaultFeatureGridLayerName, + defaultFeatureStyle, + defaultHighlightStyle, + defaultSelectStyle, + RgCommonGridProps +} from '../commonGrid'; + +type OwnProps = { onRowSelectionChange?: (selectedRowKeys: Array, selectedFeatures: OlFeature[]) => void; -} - -interface FeatureGridState { - selectedRowKeys: Key[]; -} - -export type FeatureGridProps = OwnProps & TableProps; - -/** - * The FeatureGrid. - * - * @class The FeatureGrid - * @extends React.Component - */ -export class FeatureGrid extends React.Component { - - /** - * The default properties. - */ - static defaultProps = { - features: [], - attributeBlacklist: [], - featureStyle: new OlStyle({ - fill: new OlStyleFill({ - color: 'rgba(255, 255, 255, 0.5)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }), - image: new OlStyleCircle({ - radius: 6, - fill: new OlStyleFill({ - color: 'rgba(255, 255, 255, 0.5)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }) - }) - }), - highlightStyle: new OlStyle({ - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }), - image: new OlStyleCircle({ - radius: 6, - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 1 - }) - }) - }), - selectStyle: new OlStyle({ - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 2 - }), - image: new OlStyleCircle({ - radius: 6, - fill: new OlStyleFill({ - color: 'rgba(230, 247, 255, 0.8)' - }), - stroke: new OlStyleStroke({ - color: 'rgba(73, 139, 170, 0.9)', - width: 2 - }) - }) +}; + +export type FeatureGridProps = OwnProps & RgCommonGridProps & TableProps; + +const defaultClassName = `${CSS_PREFIX}feature-grid`; + +const defaultRowClassName = `${CSS_PREFIX}feature-grid-row`; + +const rowKeyClassNamePrefix = 'row-key-'; + +const cellRowHoverClassName = 'ant-table-cell-row-hover'; + +export const FeatureGrid = ({ + attributeBlacklist = [], + children, + className, + columns, + featureStyle = defaultFeatureStyle, + features = [], + highlightStyle = defaultHighlightStyle, + keyFunction = getUid, + layerName = defaultFeatureGridLayerName, + onRowClick: onRowClickProp, + onRowMouseOut: onRowMouseOutProp, + onRowMouseOver: onRowMouseOverProp, + onRowSelectionChange, + rowClassName, + selectStyle = defaultSelectStyle, + selectable = false, + zoomToExtent = false, + ...passThroughProps +}: FeatureGridProps): React.ReactElement | null => { + + type InternalTableRecord = (T & {key?: string}); + + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + + const map = useMap(); + + const layer = useOlLayer(() => new OlLayerVector({ + properties: { + name: layerName + }, + source: new OlSourceVector({ + features: features }), - layerName: 'react-geo-feature-grid-layer', - columnDefs: {}, - keyFunction: getUid, - zoomToExtent: false, - selectable: false - }; - - /** - * The class name to add to this component. - * @private - */ - _className = 'react-geo-feature-grid'; - - /** - * The class name to add to each table row. - * @private - */ - _rowClassName = 'react-geo-feature-grid-row'; - - /** - * The prefix to use for each table row class. - * @private - */ - _rowKeyClassNamePrefix = 'row-key-'; - - /** - * The hover class name. - * @private - */ - _rowHoverClassName = 'row-hover'; - - /** - * The source holding the features of the grid. - * @private - */ - _source: OlSourceVector | null = null; - - /** - * The layer representing the features of the grid. - * @private - */ - _layer: OlLayerVector | null = null; - - /** - * The constructor. - */ - constructor(props: FeatureGridProps) { - super(props); - - this.state = { - selectedRowKeys: [] - }; - } + style: featureStyle + }), []); /** - * Called on lifecycle phase componentDidMount. + * Selects the selected feature in the map and in the grid. */ - componentDidMount() { - const { - map, - features, - zoomToExtent - } = this.props; - + const onMapSingleClick = useCallback((olEvt: OlMapBrowserEvent) => { if (!map) { return; } - this.initVectorLayer(map); - this.initMapEventHandlers(map); - - if (zoomToExtent) { - this.zoomToFeatures(features); - } - } - - /** - * Invoked immediately after updating occurs. This method is not called for - * the initial render. - * - * @param prevProps The previous props. - */ - componentDidUpdate(prevProps: FeatureGridProps) { - const { - map, - features, - selectable, - zoomToExtent - } = this.props; - - if (map && prevProps.map !== map) { - this.initVectorLayer(map); - this.initMapEventHandlers(map); - } - - if (!(_isEqual(prevProps.features, features))) { - if (this._source) { - this._source.clear(); - this._source.addFeatures(features); - } + const selectedFeatures = (map.getFeaturesAtPixel(olEvt.pixel, { + layerFilter: (layerCand: OlLayerBase) => layerCand === layer + }) || []) as OlFeature[]; - if (zoomToExtent) { - this.zoomToFeatures(features); - } - } + let rowKeys = [...selectedRowKeys]; - if (!(_isEqual(prevProps.selectable, selectable))) { - if (selectable && map) { - map.on('singleclick', this.onMapSingleClick); + selectedFeatures.forEach(selectedFeature => { + const key = keyFunction(selectedFeature); + if (rowKeys.includes(key)) { + rowKeys = rowKeys.filter(rowKey => rowKey !== key); + selectedFeature.setStyle(undefined); } else { - this.setState({ - selectedRowKeys: [] - }, () => { - if (map) { - map.un('singleclick', this.onMapSingleClick); - } - }); + rowKeys.push(key); + selectedFeature.setStyle(selectStyle); } - } - } - - /** - * Called on lifecycle phase componentWillUnmount. - */ - componentWillUnmount() { - this.deinitVectorLayer(); - this.deinitMapEventHandlers(); - } - - /** - * Initialized the vector layer that will be used to draw the input features - * on and adds it to the map (if any). - * - * @param map The map to add the layer to. - */ - initVectorLayer = (map: OlMap) => { - const { - features, - featureStyle, - layerName - } = this.props; - - if (MapUtil.getLayerByName(map, layerName)) { - return; - } - - const source = new OlSourceVector({ - features: features }); - const layer = new OlLayerVector({ - properties: { - name: layerName - }, - source: source, - style: featureStyle - }); - - map.addLayer(layer); + setSelectedRowKeys(rowKeys); + }, [keyFunction, layer, map, selectStyle, selectedRowKeys]); - this._source = source; - this._layer = layer; - }; - - /** - * Adds map event callbacks to highlight and select features in the map (if - * given) on pointermove and singleclick. Hovered and selected features will - * be highlighted and selected in the grid as well. - * - * @param map The map to register the handlers to. - */ - initMapEventHandlers = (map: OlMap) => { - const { - selectable - } = this.props; - - map.on('pointermove', this.onMapPointerMove); - - if (selectable) { - map.on('singleclick', this.onMapSingleClick); - } - }; /** * Highlights the feature beneath the cursor on the map and in the grid. - * - * @param olEvt The ol event. */ - onMapPointerMove = (olEvt: OlMapBrowserEvent) => { - const { - map, - features, - highlightStyle, - selectStyle - } = this.props; - - const { - selectedRowKeys - } = this.state; - + const onMapPointerMove = useCallback((olEvt: OlMapBrowserEvent) => { if (!map) { return; } const selectedFeatures = map.getFeaturesAtPixel(olEvt.pixel, { - layerFilter: (layerCand: OlLayerBase) => layerCand === this._layer + layerFilter: (layerCand: OlLayerBase) => layerCand === layer }) || []; features.forEach(feature => { - const key = _kebabCase(this.props.keyFunction(feature)); - const sel = `.${this._rowClassName}.${this._rowKeyClassNamePrefix}${key}`; - const el = document.querySelectorAll(sel)[0]; - if (el) { - el.classList.remove(this._rowHoverClassName); - } + const key = _kebabCase(keyFunction(feature)); + const sel = `.${defaultRowClassName}.${rowKeyClassNamePrefix}${key} > td`; + const els = document.querySelectorAll(sel); + els.forEach(el => el.classList.remove(cellRowHoverClassName)); + if (selectedRowKeys.includes(key)) { feature.setStyle(selectStyle); } else { @@ -403,106 +136,85 @@ export class FeatureGrid extends React.Component td`; + const els = document.querySelectorAll(sel); + els.forEach(el => el.classList.add(cellRowHoverClassName)); + feature.setStyle(highlightStyle); }); - }; + }, [features, highlightStyle, keyFunction, layer, map, selectStyle, selectedRowKeys]); /** - * Selects the selected feature in the map and in the grid. - * - * @param olEvt The ol event. + * Fits the map's view to the extent of the passed features. */ - onMapSingleClick = (olEvt: OlMapBrowserEvent) => { - const { - map, - selectStyle - } = this.props; - - const { - selectedRowKeys - } = this.state; - + const zoomToFeatures = useCallback((feats: OlFeature[]) => { if (!map) { return; } - const selectedFeatures = (map.getFeaturesAtPixel(olEvt.pixel, { - layerFilter: (layerCand: OlLayerBase) => layerCand === this._layer - }) || []) as OlFeature[]; + const featGeometries = feats + .map(f => f.getGeometry()) + .filter((f): f is OlGeometry => !_isNil(f)); - let rowKeys = [...selectedRowKeys]; + if (featGeometries.length > 0) { + const geomCollection = new OlGeometryCollection(featGeometries); + map.getView().fit(geomCollection.getExtent()); + } + }, [map]); - selectedFeatures.forEach(selectedFeature => { - const key = this.props.keyFunction(selectedFeature); - if (rowKeys.includes(key)) { - rowKeys = rowKeys.filter(rowKey => rowKey !== key); - selectedFeature.setStyle(undefined); - } else { - rowKeys.push(key); - selectedFeature.setStyle(selectStyle); - } - }); + useEffect(() => { + if (!map) { + return; + } - this.setState({ - selectedRowKeys: rowKeys - }); - }; + map.on('pointermove', onMapPointerMove); - /** - * Removes the vector layer from the given map (if any). - */ - deinitVectorLayer = () => { - const { - map - } = this.props; + if (selectable) { + map.on('singleclick', onMapSingleClick); + } - if (!map || !this._layer) { - return; + if (zoomToExtent) { + zoomToFeatures(features); } - map.removeLayer(this._layer); - }; + return () => { + map.un('pointermove', onMapPointerMove); - /** - * Unbinds the pointermove and click event handlers from the map (if given). - */ - deinitMapEventHandlers = () => { - const { - map, - selectable - } = this.props; + if (selectable) { + map.un('singleclick', onMapSingleClick); + } + }; + }, [features, map, onMapPointerMove, onMapSingleClick, selectable, zoomToExtent, zoomToFeatures]); + + useEffect(() => { + layer?.getSource()?.clear(); + layer?.getSource()?.addFeatures(features); + if (zoomToExtent) { + zoomToFeatures(features); + } + }, [features, layer, zoomToExtent, zoomToFeatures]); + + useEffect(() => { if (!map) { return; } - map.un('pointermove', this.onMapPointerMove); - if (selectable) { - map.un('singleclick', this.onMapSingleClick); + map.on('singleclick', onMapSingleClick); + } else { + map.un('singleclick', onMapSingleClick); } - }; + }, [map, onMapSingleClick, selectable]); /** * Returns the column definitions out of the attributes of the first * given feature. - * - * @return The column definitions. */ - getColumnDefs = () => { - const { - attributeBlacklist, - features, - columnDefs - } = this.props; - - const columns: any[] = []; + const getColumnDefs = () => { + const columnDefs: ColumnsType = []; if (features.length < 1) { return; } @@ -520,31 +232,21 @@ export class FeatureGrid extends React.Component (col as ColumnType).dataIndex === key) }); }); - return columns; + return columnDefs; }; /** * Returns the table row data from all the given features. - * - * @return The table data. */ - getTableData = (): { - key: string; - [index: string]: any; - }[] => { - const { - features - } = this.props; - + const getTableData = (): InternalTableRecord[] => { return features.map(feature => { const properties = feature.getProperties(); const filtered: typeof properties = Object.keys(properties) @@ -555,150 +257,94 @@ export class FeatureGrid extends React.Component => { - const { - features, - keyFunction - } = this.props; - + const getFeatureFromRowKey = (key: number | string | bigint): OlFeature => { const feature = features.filter(f => keyFunction(f) === key); return feature[0]; }; /** - * Called on row click and zooms the the corresponding feature's extent. - * - * @param row The clicked row. + * Called on row click and zooms the corresponding feature's extent. */ - onRowClick = (row: any) => { - const { - onRowClick - } = this.props; + const onRowClick = (row: InternalTableRecord) => { + if (!row.key) { + return; + } - const feature = this.getFeatureFromRowKey(row.key); + const feature = getFeatureFromRowKey(row.key); - if (_isFunction(onRowClick)) { - onRowClick(row, feature); + if (_isFunction(onRowClickProp)) { + onRowClickProp(row, feature); } - this.zoomToFeatures([feature]); + zoomToFeatures([feature]); }; /** * Called on row mouseover and hightlights the corresponding feature's * geometry. - * - * @param row The highlighted row. */ - onRowMouseOver = (row: any) => { - const { - onRowMouseOver - } = this.props; - - const feature = this.getFeatureFromRowKey(row.key); - - if (_isFunction(onRowMouseOver)) { - onRowMouseOver(row, feature); + const onRowMouseOver = (row: InternalTableRecord) => { + if (!row.key) { + return; } - this.highlightFeatures([feature]); - }; + const feature = getFeatureFromRowKey(row.key); - /** - * Called on mouseout and unhightlights any highlighted feature. - * - * @param row The unhighlighted row. - */ - onRowMouseOut = (row: any) => { - const { - onRowMouseOut - } = this.props; - - const feature = this.getFeatureFromRowKey(row.key); - - if (_isFunction(onRowMouseOut)) { - onRowMouseOut(row, feature); + if (_isFunction(onRowMouseOverProp)) { + onRowMouseOverProp(row, feature); } - this.unhighlightFeatures([feature]); + highlightFeatures([feature]); }; /** - * Fits the map's view to the extent of the passed features. - * - * @param features The features to zoom to. + * Called on mouseout and un-hightlights any highlighted feature. */ - zoomToFeatures = (features: OlFeature[]) => { - const { - map - } = this.props; - - if (!map) { + const onRowMouseOut = (row: InternalTableRecord) => { + if (!row.key) { return; } - const featGeometries = features - .map(f => f.getGeometry()) - .filter((f): f is OlGeometry => !_isNil(f)); + const feature = getFeatureFromRowKey(row.key); - if (featGeometries.length > 0) { - const geomCollection = new OlGeometryCollection(featGeometries); - map.getView().fit(geomCollection.getExtent()); + if (_isFunction(onRowMouseOutProp)) { + onRowMouseOutProp(row, feature); } + + unhighlightFeatures([feature]); }; /** * Highlights the given features in the map. - * - * @param highlightFeatures The features to highlight. */ - highlightFeatures = (highlightFeatures: OlFeature[]) => { - const { - map, - highlightStyle - } = this.props; - + const highlightFeatures = (feats: OlFeature[]) => { if (!map) { return; } - highlightFeatures.forEach(feature => feature.setStyle(highlightStyle)); + feats.forEach(feature => feature.setStyle(highlightStyle)); }; /** * Unhighlights the given features in the map. - * - * @param unhighlightFeatures The features to unhighlight. */ - unhighlightFeatures = (unhighlightFeatures: OlFeature[]) => { - const { - map, - selectStyle - } = this.props; - - const { - selectedRowKeys - } = this.state; - + const unhighlightFeatures = (feats: OlFeature[]) => { if (!map) { return; } - unhighlightFeatures.forEach(feature => { - const key = this.props.keyFunction(feature); + feats.forEach(feature => { + const key = keyFunction(feature); if (selectedRowKeys.includes(key)) { feature.setStyle(selectStyle); } else { @@ -710,30 +356,20 @@ export class FeatureGrid extends React.Component[]) => { - const { - map, - selectStyle - } = this.props; - + const selectFeatures = (feats: OlFeature[]) => { if (!map) { return; } - features.forEach(feature => feature.setStyle(selectStyle)); + feats.forEach(feat => feat.setStyle(selectStyle)); }; /** * Resets the style of all features. */ - resetFeatureStyles = () => { - const { - map, - features - } = this.props; - + const resetFeatureStyles = () => { if (!map) { return; } @@ -743,90 +379,56 @@ export class FeatureGrid extends React.Component { - const { - onRowSelectionChange - } = this.props; - - const selectedFeatures = selectedRowKeys.map(key => this.getFeatureFromRowKey(key)); + const onSelectChange = (rowKeys: Key[]) => { + const selectedFeatures = rowKeys.map(key => getFeatureFromRowKey(key)); if (_isFunction(onRowSelectionChange)) { - onRowSelectionChange(selectedRowKeys, selectedFeatures); + onRowSelectionChange(rowKeys, selectedFeatures); } - this.resetFeatureStyles(); - this.selectFeatures(selectedFeatures); - this.setState({ selectedRowKeys }); + resetFeatureStyles(); + selectFeatures(selectedFeatures); + setSelectedRowKeys(rowKeys); }; - /** - * The render method. - */ - render() { - const { - className, - rowClassName, - features, - map, - attributeBlacklist, - onRowClick, - onRowMouseOver, - onRowMouseOut, - zoomToExtent, - selectable, - featureStyle, - highlightStyle, - selectStyle, - layerName, - columnDefs, - children, - ...passThroughProps - } = this.props; - - const { - selectedRowKeys - } = this.state; - - const rowSelection = { - selectedRowKeys, - onChange: this.onSelectChange - }; - - const finalClassName = className - ? `${className} ${this._className}` - : this._className; - - let rowClassNameFn: (record: any) => string; - if (_isFunction(rowClassName)) { - rowClassNameFn = record => `${this._rowClassName} ${(rowClassName as ((r: any) => string))(record)}`; - } else { - const finalRowClassName = rowClassName - ? `${rowClassName} ${this._rowClassName}` - : this._rowClassName; - rowClassNameFn = record => `${finalRowClassName} ${this._rowKeyClassNamePrefix}${_kebabCase(record.key)}`; - } + const rowSelection = { + selectedRowKeys, + onChange: onSelectChange + }; - return ( - ({ - onClick: () => this.onRowClick(record), - onMouseOver: () => this.onRowMouseOver(record), - onMouseOut: () => this.onRowMouseOut(record) - })} - rowClassName={rowClassNameFn} - rowSelection={selectable ? rowSelection : undefined} - {...passThroughProps} - > - {children} -
- ); + const finalClassName = className + ? `${className} ${defaultClassName}` + : defaultClassName; + + let rowClassNameFn: (record: InternalTableRecord) => string; + if (_isFunction(rowClassName)) { + const rwcFn = rowClassName as ((r: InternalTableRecord) => string); + rowClassNameFn = record => `${defaultRowClassName} ${rwcFn(record)}`; + } else { + const finalRowClassName = rowClassName + ? `${rowClassName} ${defaultRowClassName}` + : defaultRowClassName; + rowClassNameFn = record => `${finalRowClassName} ${rowKeyClassNamePrefix}${_kebabCase(record.key)}`; } -} + + return ( + ({ + onClick: () => onRowClick(record), + onMouseOver: () => onRowMouseOver(record), + onMouseOut: () => onRowMouseOut(record) + })} + rowClassName={rowClassNameFn} + rowSelection={selectable ? rowSelection : undefined} + {...passThroughProps} + > + {children} +
+ ); +}; export default FeatureGrid; diff --git a/src/Grid/commonGrid.ts b/src/Grid/commonGrid.ts new file mode 100644 index 0000000000..0237928a44 --- /dev/null +++ b/src/Grid/commonGrid.ts @@ -0,0 +1,132 @@ +import OlFeature from 'ol/Feature'; +import OlGeometry from 'ol/geom/Geometry'; +import OlStyleCircle from 'ol/style/Circle'; +import OlStyleFill from 'ol/style/Fill'; +import OlStyleStroke from 'ol/style/Stroke'; +import OlStyle from 'ol/style/Style'; + +export type RgCommonGridProps = { + /** + * The features to show in the grid and the map (if set). + */ + features?: OlFeature[]; + /** + */ + attributeBlacklist?: string[]; + /** + * The default style to apply to the features. + */ + featureStyle?: OlStyle | (() => OlStyle); + /** + * The highlight style to apply to the features. + */ + highlightStyle?: OlStyle | (() => OlStyle); + /** + * The select style to apply to the features. + */ + selectStyle?: OlStyle | (() => OlStyle); + /** + * The name of the vector layer presenting the features in the grid. + */ + layerName?: string; + /** + * A Function that creates the row key from the given feature. + * Receives the feature as property. + * Default is: feature => feature.ol_uid + */ + keyFunction?: (feature: OlFeature) => string; + /** + * Whether the map should center on the current feature's extent on init or + * not. + */ + zoomToExtent?: boolean; + /** + * Whether rows and features should be selectable or not. + */ + selectable?: boolean; + /** + * A CSS class which should be added to the table. + */ + className?: string; + /** + * A CSS class to add to each table row or a function that + * is evaluated for each record. + */ + rowClassName?: string | ((record: RowType) => string); + /** + * Callback function, that will be called on rowclick. + */ + onRowClick?: (row: RowType, feature: OlFeature, additionalArgs?: any) => void; + /** + * Callback function, that will be called on rowmouseover. + */ + onRowMouseOver?: (row: RowType, feature: OlFeature, additionalArgs?: any) => void; + /** + * Callback function, that will be called on rowmouseout. + */ + onRowMouseOut?: (row: RowType, feature: OlFeature, additionalArgs?: any) => void; +}; + +export const defaultFeatureGridLayerName: string = 'react-geo-feature-grid-layer'; + +export const defaultFeatureStyle = new OlStyle({ + fill: new OlStyleFill({ + color: 'rgba(255, 255, 255, 0.5)' + }), + stroke: new OlStyleStroke({ + color: 'rgba(73, 139, 170, 0.9)', + width: 1 + }), + image: new OlStyleCircle({ + radius: 6, + fill: new OlStyleFill({ + color: 'rgba(255, 255, 255, 0.5)' + }), + stroke: new OlStyleStroke({ + color: 'rgba(73, 139, 170, 0.9)', + width: 1 + }) + }) +}); + +export const highlightFillColor: string = 'rgba(230, 247, 255, 0.8)'; + +export const defaultHighlightStyle = new OlStyle({ + fill: new OlStyleFill({ + color: highlightFillColor + }), + stroke: new OlStyleStroke({ + color: 'rgba(73, 139, 170, 0.9)', + width: 1 + }), + image: new OlStyleCircle({ + radius: 6, + fill: new OlStyleFill({ + color: 'rgba(230, 247, 255, 0.8)' + }), + stroke: new OlStyleStroke({ + color: 'rgba(73, 139, 170, 0.9)', + width: 1 + }) + }) +}); + +export const defaultSelectStyle = new OlStyle({ + fill: new OlStyleFill({ + color: 'rgba(230, 247, 255, 0.8)' + }), + stroke: new OlStyleStroke({ + color: 'rgba(73, 139, 170, 0.9)', + width: 2 + }), + image: new OlStyleCircle({ + radius: 6, + fill: new OlStyleFill({ + color: 'rgba(230, 247, 255, 0.8)' + }), + stroke: new OlStyleStroke({ + color: 'rgba(73, 139, 170, 0.9)', + width: 2 + }) + }) +});