diff --git a/backend/src/main/resources/db/migration/V1__create_schema.sql b/backend/src/main/resources/db/migration/V1__create_schema.sql
index d40f9e76..660adddf 100644
--- a/backend/src/main/resources/db/migration/V1__create_schema.sql
+++ b/backend/src/main/resources/db/migration/V1__create_schema.sql
@@ -44,3 +44,19 @@ CREATE TABLE IF NOT EXISTS silva.oracle_extraction_logs (
CONSTRAINT oracle_extraction_logs_pk
PRIMARY KEY(id)
);
+
+-- Create sequence if it doesn't exist for User Recent Openings
+CREATE SEQUENCE IF NOT EXISTS silva.user_recent_openings_seq
+START WITH 1
+INCREMENT BY 1
+MINVALUE 1
+NO MAXVALUE
+CACHE 30;
+
+-- Use the sequence in your table creation or insert statements
+CREATE TABLE IF NOT EXISTS silva.user_recent_openings (
+ id BIGINT PRIMARY KEY DEFAULT nextval('silva.user_recent_openings_seq'),
+ opening_id VARCHAR(255) NOT NULL,
+ user_id VARCHAR(255) NOT NULL,
+ last_viewed TIMESTAMP DEFAULT NOW()
+);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ccf6c592..86a1ab75 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -2706,7 +2706,6 @@
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
- "license": "ISC",
"peer": true,
"dependencies": {
"get-stream": "^6.0.1",
@@ -2720,7 +2719,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz",
"integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==",
- "license": "ISC",
"peer": true
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
@@ -2736,7 +2734,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz",
"integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==",
- "license": "BSD-3-Clause",
"peer": true,
"peerDependencies": {
"mapbox-gl": ">=0.32.1 <2.0.0"
@@ -2746,28 +2743,24 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
- "license": "ISC",
"peer": true
},
"node_modules/@mapbox/tiny-sdf": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz",
"integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==",
- "license": "BSD-2-Clause",
"peer": true
},
"node_modules/@mapbox/unitbezier": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
"integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==",
- "license": "BSD-2-Clause",
"peer": true
},
"node_modules/@mapbox/vector-tile": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
- "license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@mapbox/point-geometry": "~0.1.0"
@@ -2777,7 +2770,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
- "license": "ISC",
"peer": true,
"engines": {
"node": ">=6.0.0"
@@ -3118,6 +3110,14 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.28",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
+ "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
+ "dev": true,
+ "optional": true,
+ "peer": true
+ },
"node_modules/@react-leaflet/core": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
@@ -3160,18 +3160,6 @@
}
}
},
- "node_modules/@rollup/pluginutils/node_modules/picomatch": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
- "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.3.tgz",
@@ -4672,7 +4660,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@terraformer/arcgis/-/arcgis-2.1.2.tgz",
"integrity": "sha512-IvdfqehcNAUtKU1OFMKwPT8EvdKlVFZ7q7ZKzkIF8XzYZIVsZLuXuOS1UBdRh5u/o+X5Gax7jiZhD8U/4TV+Jw==",
- "license": "MIT",
"peer": true,
"dependencies": {
"@terraformer/common": "^2.1.2"
@@ -4682,7 +4669,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@terraformer/common/-/common-2.1.2.tgz",
"integrity": "sha512-cwPdTFzIpekZhZRrgDEkqLKNPoqbyCBQHiemaovnGIeUx0Pl336MY/eCxzJ5zXkrQLVo9zPalq/vYW5HnyKevQ==",
- "license": "MIT",
"peer": true
},
"node_modules/@testing-library/dom": {
@@ -5725,6 +5711,29 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@vitest/ui": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.4.tgz",
+ "integrity": "sha512-Zd9e5oU063c+j9N9XzGJagCLNvG71x/2tOme3Js4JEZKX55zsgxhJwUgLI8hkN6NjMLpdJO8d7nVUUuPGAA58Q==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@vitest/utils": "2.1.4",
+ "fflate": "^0.8.2",
+ "flatted": "^3.3.1",
+ "pathe": "^1.1.2",
+ "sirv": "^3.0.0",
+ "tinyglobby": "^0.2.9",
+ "tinyrainbow": "^1.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "2.1.4"
+ }
+ },
"node_modules/@vitest/utils": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz",
@@ -6626,7 +6635,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
"integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
- "license": "MIT",
"peer": true
},
"node_modules/cssstyle": {
@@ -7385,7 +7393,6 @@
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
- "license": "ISC",
"peer": true
},
"node_modules/eastasianwidth": {
@@ -8386,7 +8393,6 @@
"version": "3.0.13",
"resolved": "https://registry.npmjs.org/esri-leaflet/-/esri-leaflet-3.0.13.tgz",
"integrity": "sha512-QP831w3vv2Hfy8aWUcUHJShSrg+EeIt5vxtTJZEHbgLzjS89QidEo1GrZ51u5KAIMuq8WPmNAurstU2AaCPS8g==",
- "license": "Apache-2.0",
"peer": true,
"dependencies": {
"@terraformer/arcgis": "^2.1.0",
@@ -8400,7 +8406,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/esri-leaflet-cluster/-/esri-leaflet-cluster-3.0.1.tgz",
"integrity": "sha512-rqI4inhOzqZqGj0nfYNdfnDI51mGEpdefw4M4WoA0ig1FVh0V5W9ecRuHXbaBim+R7SSKMwY30S2wKipcYoF2w==",
- "license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"esri-leaflet": "*",
@@ -8412,7 +8417,6 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/esri-leaflet-geocoder/-/esri-leaflet-geocoder-3.1.5.tgz",
"integrity": "sha512-NYPPLJ2gYb8yf3wrUBxLNlyxv9yT1E3Q9rYGuaxRzCUaU/ULsRzs9vm6cE4xDWhO8g+9j5DUIi3zWMMKRyeBcw==",
- "license": "Apache-2.0",
"peer": true,
"dependencies": {
"esri-leaflet": "^3.0.2",
@@ -8423,7 +8427,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/esri-leaflet-heatmap/-/esri-leaflet-heatmap-2.0.1.tgz",
"integrity": "sha512-Yd6gNghJfG5Zv7+5A0SraqIMK6nqYSAkF925DXnpbi5BFbI8vbJFK8JyG9hFq+vC/iBtCCpQyGz0UjtHMzNrPg==",
- "license": "Apache-2.0",
"peer": true,
"dependencies": {
"leaflet": "^1.0.0-rc.3",
@@ -8437,7 +8440,6 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/esri-leaflet-vector/-/esri-leaflet-vector-3.1.5.tgz",
"integrity": "sha512-XNRkrqfB4xKGQfRtjiMJsxwF4oiPnSNngQJrLBbMQMadLqcy+mZRAbDHDx/KEK6S0w0QoM4/A+A2rcNHHBQKlA==",
- "license": "Apache-2.0",
"peer": true,
"dependencies": {
"mapbox-gl": "1.13.1"
@@ -8583,6 +8585,22 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz",
+ "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
@@ -8792,7 +8810,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
"integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==",
- "license": "ISC",
"peer": true
},
"node_modules/get-intrinsic": {
@@ -8818,7 +8835,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -8862,7 +8878,6 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
- "license": "MIT",
"peer": true
},
"node_modules/glob": {
@@ -9016,7 +9031,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
"integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
- "license": "ISC",
"peer": true
},
"node_modules/has-bigints": {
@@ -9999,6 +10013,18 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/jest-util/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@@ -10182,7 +10208,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
"integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==",
- "license": "ISC",
"peer": true
},
"node_modules/keyv": {
@@ -10231,7 +10256,6 @@
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz",
"integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==",
- "license": "MIT",
"peer": true,
"peerDependencies": {
"leaflet": "^1.3.1"
@@ -10424,7 +10448,6 @@
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.1.tgz",
"integrity": "sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g==",
- "license": "SEE LICENSE IN LICENSE.txt",
"peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.0",
@@ -10478,6 +10501,17 @@
"node": ">=8.6"
}
},
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -10544,6 +10578,17 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mrmime": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
+ "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -10554,7 +10599,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
- "license": "MIT",
"peer": true
},
"node_modules/nanoid": {
@@ -10950,7 +10994,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
- "license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"ieee754": "^1.1.12",
@@ -10974,12 +11017,11 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
- "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "license": "MIT",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -11027,7 +11069,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
- "license": "ISC",
"peer": true
},
"node_modules/prelude-ls": {
@@ -11096,7 +11137,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
- "license": "MIT",
"peer": true
},
"node_modules/proxy-from-env": {
@@ -11155,7 +11195,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
- "license": "ISC",
"peer": true
},
"node_modules/raf": {
@@ -11416,7 +11455,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
- "license": "MIT",
"peer": true,
"dependencies": {
"protocol-buffers-schema": "^3.3.1"
@@ -11796,6 +11834,22 @@
"is-arrayish": "^0.3.1"
}
},
+ "node_modules/sirv": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz",
+ "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -12152,7 +12206,6 @@
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
- "license": "ISC",
"peer": true,
"dependencies": {
"kdbush": "^3.0.0"
@@ -12303,7 +12356,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-binary-search/-/tiny-binary-search-1.0.3.tgz",
"integrity": "sha512-STSHX/L5nI9WTLv6wrzJbAPbO7OIISX83KFBh2GVbX1Uz/vgZOU/ANn/8iV6t35yMTpoPzzO+3OQid3mifE0CA==",
- "license": "Apache-2.0",
"peer": true
},
"node_modules/tinybench": {
@@ -12320,6 +12372,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz",
+ "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "fdir": "^6.4.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/tinypool": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz",
@@ -12334,7 +12401,6 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
- "license": "ISC",
"peer": true
},
"node_modules/tinyrainbow": {
@@ -12415,6 +12481,17 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tough-cookie": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz",
@@ -12914,7 +12991,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
- "license": "MIT",
"peer": true,
"dependencies": {
"@mapbox/point-geometry": "0.1.0",
diff --git a/frontend/src/__test__/components/ActionButtons.test.tsx b/frontend/src/__test__/components/ActionButtons.test.tsx
new file mode 100644
index 00000000..6f554d9b
--- /dev/null
+++ b/frontend/src/__test__/components/ActionButtons.test.tsx
@@ -0,0 +1,39 @@
+// ActionButtons.test.tsx
+import React from "react";
+import { vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import ActionButtons from "../../components/ActionButtons";
+
+// Mock console.log
+const consoleLogMock = vi.spyOn(console, "log").mockImplementationOnce(() =>vi.fn()) ;
+
+afterEach(() => {
+ consoleLogMock.mockClear();
+});
+
+afterAll(() => {
+ consoleLogMock.mockRestore();
+});
+
+describe("ActionButtons", () => {
+ const rowId = "test-row-id";
+
+ it("renders the 'View' and 'Document Download' buttons", () => {
+ render();
+
+ // Check that both buttons are in the document
+ expect(screen.getByRole("button", { name: /View/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Document Download/i })).toBeInTheDocument();
+ });
+
+ it("calls console.log with rowId when the 'View' button is clicked", () => {
+ render();
+
+ // Find the "View" button and click it
+ const viewButton = screen.getByRole("button", { name: /View/i });
+ fireEvent.click(viewButton);
+
+ // Check if console.log was called with the correct rowId
+ expect(consoleLogMock).toHaveBeenCalledWith(rowId);
+ });
+});
diff --git a/frontend/src/__test__/components/BarChartGrouped.test.tsx b/frontend/src/__test__/components/BarChartGrouped.test.tsx
index 16255636..f1d12e98 100644
--- a/frontend/src/__test__/components/BarChartGrouped.test.tsx
+++ b/frontend/src/__test__/components/BarChartGrouped.test.tsx
@@ -1,26 +1,40 @@
import React from 'react';
-import { render, screen, waitFor } from '@testing-library/react';
-import { describe, expect, it, vi } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import BarChartGrouped from '../../components/BarChartGrouped';
-import { fetchOpeningsPerYear } from '../../services/OpeningService';
-
-vi.mock('../../services/OpeningService', () => ({
- fetchOpeningsPerYear: vi.fn(() => Promise.resolve([
- { group: '2022', key: 'Openings', value: 10 },
- { group: '2023', key: 'Openings', value: 15 },
- ])),
+import { useDistrictListQuery, useFetchOpeningsPerYear } from '../../services/queries/dashboard/dashboardQueries';
+import { describe, expect, it } from 'vitest';
+import { vi } from 'vitest';
+import '@testing-library/jest-dom';
+// Mock the hook
+vi.mock('../../services/queries/dashboard/dashboardQueries', () => ({
+ useFetchOpeningsPerYear: vi.fn(),
+ useDistrictListQuery: vi.fn(),
}));
-describe('BarChartGrouped component tests', () => {
- it('should render loading state while fetching data and clean it after', async () => {
- render();
+const queryClient = new QueryClient();
- const element = await waitFor(() => screen.getByText('Loading...'));
+describe('BarChartGrouped component', () => {
+ it('should display loading state when data is fetching', () => {
+ // Mock loading state for openings data
+ (useFetchOpeningsPerYear as any).mockReturnValue({
+ data: [],
+ isLoading: true,
+ });
- expect(element).toBeDefined();
-
- expect(fetchOpeningsPerYear).toHaveBeenCalled();
- expect(screen.queryByTestId('bar-chart')).toBeDefined();
- });
+ // If you're using useDistrictListQuery, mock it too
+ (useDistrictListQuery as any).mockReturnValue({
+ data: [],
+ isLoading: false,
+ });
-});
+ render(
+
+
+
+ );
+
+ // Check if loading text is displayed
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx
new file mode 100644
index 00000000..cb8c535f
--- /dev/null
+++ b/frontend/src/__test__/components/Dashboard/Opening/RecentOpeningsDataTable.test.tsx
@@ -0,0 +1,69 @@
+// src/__test__/components/SilvicultureSearch/Openings/OpeningsSearchBar.test.tsx
+
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom";
+import { vi } from "vitest";
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import PaginationProvider from "../../../../contexts/PaginationProvider";
+import RecentOpeningsDataTable from "../../../../components/Dashboard/Opening/RecentOpeningsDataTable";
+import { MemoryRouter } from "react-router-dom";
+import exp from "constants";
+
+describe("OpeningsSearchBar", () => {
+ // Create a new QueryClient instance for each test
+ const queryClient = new QueryClient();
+ const handleCheckboxChange = vi.fn()
+ const setLoadId = vi.fn()
+ const toggleSpatial = vi.fn()
+ const showSpatial = false
+ const data = { data: [], perPage: 0, totalPages: 0 }
+ const headers = []
+
+ it("shows appropriate message when no data is in the table", () => {
+ render(
+
+
+
+
+
+
+
+ );
+ expect(screen.getByText(/There are no openings to show yet/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Your recent openings will appear here once you generate one/i)).toBeInTheDocument();
+ });
+
+ it("renders a blank table when rows is empty array", () => {
+ render(
+
+
+
+
+
+
+
+ );
+ // Check if the table is present
+ const table = screen.getByRole('table');
+ expect(table).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/__test__/components/OpeningScreenDataTable.test.tsx b/frontend/src/__test__/components/OpeningScreenDataTable.test.tsx
deleted file mode 100644
index 8f8bec2f..00000000
--- a/frontend/src/__test__/components/OpeningScreenDataTable.test.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react';
-import { act, render } from '@testing-library/react';
-import { describe, expect, it, vi } from 'vitest';
-import OpeningScreenDataTable from '../../components/OpeningScreenDataTable';
-import PaginationContext from '../../contexts/PaginationContext';
-import { RecentOpening } from '../../types/RecentOpening';
-
-const rows: RecentOpening[] = [{
- id: '123',
- openingId: '123',
- fileId: '1',
- cuttingPermit: '1',
- timberMark: '1',
- cutBlock: '1',
- grossAreaHa: 1,
- statusDesc: 'Approved',
- categoryDesc: 'Another:Another',
- disturbanceStart: '1',
- entryTimestamp: '1',
- updateTimestamp: '1',
-}];
-
-const headers = [{ key: 'openingId', header: 'Opening Id', },
- { key: 'forestFileId', header: 'File Id', },
- { key: 'cuttingPermit', header: 'Cutting permit', },
- { key: 'timberMark', header: 'Timber mark', },
- { key: 'cutBlock', header: 'Cut block', },
- { key: 'grossAreaHa', header: 'Gross area (ha)', },
- { key: 'status', header: 'Status', },
- { key: 'category', header: 'Category', },
- { key: 'disturbanceStart', header: 'Disturbance start', },
- { key: 'createdAt', header: 'Created At', },
- { key: 'lastViewed', header: 'Last Viewed', },
- { key: 'actions', header: 'Actions', },
-];
-
-const setOpeningId = vi.fn();
-const paginationValueMock = {
- getCurrentData: () => rows,
- currentPage: 0,
- totalPages: 0,
- handlePageChange: vi.fn(),
- handleItemsPerPageChange: vi.fn(),
- itemsPerPage: 5,
- totalResultItems:100,
- setTotalResultItems:vi.fn(),
- setPageData: vi.fn(),
- setInitialItemsPerPage: vi.fn(),
-};
-
-describe('Opening Screen Data table component test', () => {
- it('should remove the row checkbox when showSpatial is false', () => {
- const { queryByTestId } = render(
-
-
-
- );
-
- act(() => {
- const tableSelectionRow: HTMLElement | null = queryByTestId('checkbox__opening-screen-data-table_1');
- expect(tableSelectionRow).toBeNull();
- });
- });
-
- it('should display the row checkbox when showSpatial is true', () => {
- const { queryByTestId } = render(
-
-
-
- );
-
- act(() => {
- const tableSelectionRow: HTMLElement | null = queryByTestId('checkbox__opening-screen-data-table_1');
- // The next line should be "not.toBeNull()" however, Carbon React team forgot to add data-testid
- // attribute to this component (TableSelectRow), making it impossible to get by testid value.
- // Once we have that fixed, please get back here and update the next statement.
- expect(tableSelectionRow).toBeNull();
- });
- });
-});
diff --git a/frontend/src/__test__/components/OpeningsTab.test.tsx b/frontend/src/__test__/components/OpeningsTab.test.tsx
index 10bd1b32..d96aa034 100644
--- a/frontend/src/__test__/components/OpeningsTab.test.tsx
+++ b/frontend/src/__test__/components/OpeningsTab.test.tsx
@@ -4,8 +4,7 @@ import { render, act, waitFor, screen } from '@testing-library/react';
import OpeningsTab from '../../components/OpeningsTab';
import { AuthProvider } from '../../contexts/AuthProvider';
import { getWmsLayersWhitelistUsers } from '../../services/SecretsService';
-import { fetchRecentOpenings } from '../../services/OpeningService';
-import { RecentOpening } from '../../types/RecentOpening';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import PaginationProvider from '../../contexts/PaginationProvider';
@@ -20,32 +19,66 @@ vi.mock('../../services/OpeningService', async () => {
fetchRecentOpenings: vi.fn(),
};
});
-
-
-const rows: RecentOpening[] = [{
- id: '123',
- openingId: '123',
- fileId: '1',
- cuttingPermit: '1',
- timberMark: '1',
- cutBlock: '1',
- grossAreaHa: 1,
- statusDesc: 'Approved',
- categoryDesc: 'Another:Another',
- disturbanceStart: '1',
- entryTimestamp: '1',
- updateTimestamp: '1',
-}];
+const queryClient = new QueryClient();
describe('Openings Tab test',() => {
it('should render properly',async () =>{
(getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]);
- (fetchRecentOpenings as vi.Mock).mockResolvedValue(rows);
+
await act(async () => {
- render();
+ render(
+
+
+
+
+
+
+
+ );
});
expect(screen.getByText('Recent openings')).toBeInTheDocument();
});
+ it('should have Hide map when the showSpatial is true',async () =>{
+ (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{userName: 'TEST'}]);
+
+ await act(async () => {
+ render(
+
+
+
+
+
+
+
+ );
+ });
+ expect(screen.getByText('Hide map')).toBeInTheDocument();
+ });
+
+ it('should render the table', async () => {
+ // Mocking state values
+ vi.spyOn(React, 'useState')
+ .mockImplementationOnce(() => [null, vi.fn()]) // for loadId
+ .mockImplementationOnce(() => [true, vi.fn()]) // for openingPolygonNotFound
+ .mockImplementationOnce(() => [{ userName: 'TEST' }, vi.fn()]) // for wmsUsersWhitelist
+ .mockImplementationOnce(() => [[], vi.fn()]); // for headers
+
+ (getWmsLayersWhitelistUsers as vi.Mock).mockResolvedValue([{ userName: 'TEST' }]);
+
+ await act(async () => {
+ render(
+
+
+
+
+
+
+
+ );
+ });
+ expect(screen.getByRole('table')).toBeInTheDocument();
+ });
+
});
\ No newline at end of file
diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx
index 80866593..ed5e13d7 100644
--- a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx
+++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchTab.test.tsx
@@ -136,6 +136,6 @@ describe('OpeningSearchTab', () => {
const searchInput = screen.getByPlaceholderText('Search by opening ID, opening number, timber mark or file ID');
await act(async () => await userEvent.type(searchInput, 'potato'));
await act(async () => (await screen.findByTestId('search-button')).click());
- expect(screen.getByText('Results not found')).toBeInTheDocument();
+ expect(screen.getByText('There are no openings to show yet')).toBeInTheDocument();
});
});
\ No newline at end of file
diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx
index b2d2e752..7b8108dc 100644
--- a/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx
+++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/SearchScreenDataTable.test.tsx
@@ -1,15 +1,245 @@
import React from 'react';
-import { render } from '@testing-library/react';
+import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
-import SearchScreenDataTable from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/index';
-import { columns, rows } from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/testData';
+import SearchScreenDataTable from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable';
+import { columns } from '../../../../components/SilvicultureSearch/Openings/SearchScreenDataTable/headerData';
import PaginationProvider from '../../../../contexts/PaginationProvider';
import { NotificationProvider } from '../../../../contexts/NotificationProvider';
import { BrowserRouter } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { OpeningsSearchProvider } from '../../../../contexts/search/OpeningsSearch';
const handleCheckboxChange = vi.fn();
const toggleSpatial = vi.fn();
+const queryClient = new QueryClient();
+
+export const rows:any = [
+ {
+ id: '114207',
+ openingId: '114207',
+ fileId: 'TFL47',
+ cuttingPermit: '12S',
+ timberMark: '47/12S',
+ cutBlock: '12-69',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2022-10-27',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-27'
+ },
+ {
+ id: '114206',
+ openingId: '114206',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-69',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2022-09-04',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-27'
+ },
+ {
+ id: '114205',
+ openingId: '114205',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2022-09-04',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-27'
+ },
+ {
+ id: '114204',
+ openingId: '114204',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Active',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2022-01-16',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-26'
+ },
+ {
+ id: '114203',
+ openingId: '114203',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Active',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-12-08',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-26'
+ },
+ {
+ id: '114202',
+ openingId: '114202',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-11-15',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-25'
+ },
+ {
+ id: '114201',
+ openingId: '114201',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-11-15',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-25'
+ },
+ {
+ id: '114200',
+ openingId: '114200',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Active',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-10-20',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-24'
+ },
+ {
+ id: '114199',
+ openingId: '114199',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Active',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-10-20',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-24'
+ },
+ {
+ id: '114198',
+ openingId: '114198',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-09-12',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-23'
+ },
+ {
+ id: '114197',
+ openingId: '114197',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-09-12',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-23'
+ },
+ {
+ id: '114196',
+ openingId: '114196',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-08-05',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-22'
+ },
+ {
+ id: '114195',
+ openingId: '114195',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Free growing',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-08-05',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-22'
+ },
+ {
+ id: '114194',
+ openingId: '114194',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Active',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-07-10',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-21'
+ },
+ {
+ id: '114193',
+ openingId: '114193',
+ fileId: 'TFL47',
+ cuttingPermit: '12T',
+ timberMark: '47/12S',
+ cutBlock: '12-44A',
+ grossAreaHa: '12.9',
+ status: 'Active',
+ category: 'FTML',
+ disturbanceStart: '-',
+ createdAt: '2021-07-10',
+ orgUnit: 'DCC - Cariboo chilcotin natural resources',
+ lastViewed: '2022-10-21'
+ }
+];
describe('Search Screen Data table test', () => {
@@ -17,6 +247,7 @@ describe('Search Screen Data table test', () => {
const { getByText, container } =
render(
+
@@ -32,6 +263,7 @@ describe('Search Screen Data table test', () => {
+
);
@@ -45,6 +277,7 @@ describe('Search Screen Data table test', () => {
const { getByText, container } =
render(
+
@@ -60,6 +293,7 @@ describe('Search Screen Data table test', () => {
+
);
@@ -69,4 +303,32 @@ describe('Search Screen Data table test', () => {
expect(container.querySelector('.total-search-results')).toContainHTML('0');
});
+ it('should render the checkbox for showSPatial being true', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+
+ );
+ const checkbox = document.querySelector('.cds--checkbox-group');
+ expect(checkbox).toBeInTheDocument();
+
+ });
+
+
});
\ No newline at end of file
diff --git a/frontend/src/__test__/components/SpatialCheckbox.test.tsx b/frontend/src/__test__/components/SpatialCheckbox.test.tsx
new file mode 100644
index 00000000..267bbdf8
--- /dev/null
+++ b/frontend/src/__test__/components/SpatialCheckbox.test.tsx
@@ -0,0 +1,61 @@
+// SpatialCheckbox.test.tsx
+import React from "react";
+import { vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import SpatialCheckbox from "../../components/SpatialCheckbox";
+
+describe("SpatialCheckbox", () => {
+ const rowId = "test-row-id";
+ const handleRowSelectionChanged = vi.fn();
+
+ afterEach(() => {
+ handleRowSelectionChanged.mockClear();
+ });
+
+ it("renders the checkbox with the tooltip", () => {
+ render(
+
+ );
+
+ // Check if the checkbox is in the document
+ const checkbox = screen.getByRole("checkbox", {
+ name: /click to view this opening's map activity/i,
+ });
+ expect(checkbox).toBeInTheDocument();
+ });
+
+ it("sets the checkbox to 'checked' if rowId is in selectedRows", () => {
+ render(
+
+ );
+
+ const checkbox = screen.getByRole("checkbox");
+ expect(checkbox).toBeChecked();
+ });
+
+ it("calls handleRowSelectionChanged with rowId when checkbox is toggled", () => {
+ render(
+
+ );
+
+ const checkbox = screen.getByRole("checkbox");
+
+ // Simulate a click on the checkbox
+ fireEvent.click(checkbox);
+
+ // Check if handleRowSelectionChanged was called with the correct rowId
+ expect(handleRowSelectionChanged).toHaveBeenCalledWith(rowId);
+ });
+});
diff --git a/frontend/src/__test__/components/TableCellContent.test.tsx b/frontend/src/__test__/components/TableCellContent.test.tsx
new file mode 100644
index 00000000..3cebab09
--- /dev/null
+++ b/frontend/src/__test__/components/TableCellContent.test.tsx
@@ -0,0 +1,98 @@
+// TableCellContent.test.tsx
+import React from "react";
+import { vi } from "vitest";
+import { render, screen } from "@testing-library/react";
+import TableCellContent from "../../components/TableCellContent";
+import { OpeningsSearch } from "../../types/OpeningsSearch";
+
+// Mock components
+vi.mock("../StatusTag", () => ({
+ default: ({ code }: { code: string }) =>
{`Status: ${code}`}
,
+}));
+vi.mock("../ActionButtons", () => ({
+ default: ({ rowId }: { rowId: string }) => {`Actions for ${rowId}`}
,
+}));
+vi.mock("../SpatialCheckbox", () => ({
+ default: ({ rowId }: { rowId: string }) => {`Spatial Checkbox for ${rowId}`}
,
+}));
+
+describe("TableCellContent", () => {
+ const row = {
+ openingId: 1,
+ statusDescription: "Active",
+ categoryCode: "A",
+ categoryDescription: "Category A",
+ } as OpeningsSearch;
+ const selectedRows: string[] = [];
+ const handleRowSelectionChanged = vi.fn();
+
+ it("renders StatusTag when headerKey is 'statusDescription'", () => {
+ render(
+
+ );
+
+ expect(screen.getByText(/Active/i)).toBeInTheDocument();
+ });
+
+ it("renders ActionButtons and optionally SpatialCheckbox when headerKey is 'actions'", () => {
+ const { rerender } = render(
+
+ );
+ console.log(screen.debug());
+ expect(screen.queryByText(/View/i)).toBeInTheDocument();
+ });
+
+ it("renders category code and description when headerKey is 'Category'", () => {
+ render(
+
+ );
+ expect(screen.getAllByText("A - Category A")[0]).toBeInTheDocument();
+ });
+
+ it("renders default content for other headerKey values", () => {
+ render(
+
+ );
+
+ expect(screen.getByText(/Unknown Value/i)).toBeInTheDocument();
+ });
+
+ it("renders SpatialCheckbox when headerKey is 'actions' and showSpatial is true", () => {
+ render(
+
+ );
+ //check if the Checkbox text is present
+ expect(screen.getByText(/Click to view this opening's map activity./i)).toBeInTheDocument();
+ });
+
+});
diff --git a/frontend/src/__test__/components/TableRowComponent.test.tsx b/frontend/src/__test__/components/TableRowComponent.test.tsx
new file mode 100644
index 00000000..8e0a4ba2
--- /dev/null
+++ b/frontend/src/__test__/components/TableRowComponent.test.tsx
@@ -0,0 +1,88 @@
+// TableRowComponent.test.tsx
+import React from "react";
+import { vi } from "vitest";
+import { render, screen, fireEvent } from "@testing-library/react";
+import TableRowComponent from "../../components/TableRowComponent";
+import { OpeningsSearch } from "../../types/OpeningsSearch";
+import { ITableHeader } from "../../types/TableHeader";
+
+describe("TableRowComponent", () => {
+ const row: OpeningsSearch = {
+ openingId: 1,
+ statusDescription: "Active",
+ categoryCode: "A",
+ categoryDescription: "Category A",
+ };
+
+ const headers: ITableHeader[] = [
+ { key: "statusDescription", label: "Status", selected: true },
+ { key: "categoryCode", label: "Category Code", selected: true },
+ { key: "actions", label: "Actions", selected: false },
+ ];
+
+ const showSpatial = false;
+ const selectedRows: string[] = [];
+ const handleRowSelectionChanged = vi.fn();
+ const setOpeningDetails = vi.fn();
+
+ afterEach(() => {
+ handleRowSelectionChanged.mockClear();
+ setOpeningDetails.mockClear();
+ });
+
+ it("renders TableCellContent for each selected header", () => {
+ render(
+
+ );
+
+ // Check that the expected content for each selected header is rendered
+ expect(screen.getByText("Active")).toBeInTheDocument();
+ expect(screen.getByText("A")).toBeInTheDocument();
+ expect(screen.queryByText("Category A")).not.toBeInTheDocument(); // CategoryDescription isn't selected
+ });
+
+ it("calls setOpeningDetails when the row is clicked", () => {
+ render(
+
+ );
+
+ // Simulate click on the row
+ const tableRow = screen.getByRole("row");
+ fireEvent.click(tableRow);
+
+ // Verify that setOpeningDetails was called with true
+ expect(setOpeningDetails).toHaveBeenCalledWith(true);
+ });
+
+ it("renders TableCell components only for selected headers", () => {
+ render(
+
+ );
+
+ // Verify content specific to selected headers
+ expect(screen.getByText("Active")).toBeInTheDocument();
+ expect(screen.getByText("A")).toBeInTheDocument();
+ expect(screen.queryByText("Actions")).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/__test__/screens/Opening.test.tsx b/frontend/src/__test__/screens/Opening.test.tsx
index c5c623e2..70d4ba8a 100644
--- a/frontend/src/__test__/screens/Opening.test.tsx
+++ b/frontend/src/__test__/screens/Opening.test.tsx
@@ -6,6 +6,7 @@ import PaginationContext from '../../contexts/PaginationContext';
import { NotificationProvider } from '../../contexts/NotificationProvider';
import { BrowserRouter } from 'react-router-dom';
import { RecentOpening } from '../../types/RecentOpening';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getWmsLayersWhitelistUsers } from '../../services/SecretsService';
import { fetchFreeGrowingMilestones, fetchOpeningsPerYear, fetchRecentOpenings, fetchRecentActions } from '../../services/OpeningService';
import { fetchOpeningFavourites } from '../../services/OpeningFavouriteService';
@@ -72,6 +73,7 @@ const paginationValueMock = {
setPageData: vi.fn(),
setInitialItemsPerPage: vi.fn(),
};
+const queryClient = new QueryClient();
describe('Opening screen test cases', () => {
@@ -96,6 +98,7 @@ describe('Opening screen test cases', () => {
it('should renders Opening Page Title component', async () => {
const { getByTestId } = render(
+
@@ -103,6 +106,7 @@ describe('Opening screen test cases', () => {
+
);
@@ -121,6 +125,7 @@ describe('Opening screen test cases', () => {
await act(async () => {
({ container } = render(
+
@@ -128,6 +133,7 @@ describe('Opening screen test cases', () => {
+
));
});
@@ -160,6 +166,7 @@ describe('Opening screen test cases', () => {
await act(async () => {
({ container, getByText } = render(
+
@@ -167,6 +174,7 @@ describe('Opening screen test cases', () => {
+
));
});
@@ -183,6 +191,7 @@ describe('Opening screen test cases', () => {
await act(async () => {
({ container, getByText } = render(
+
@@ -190,6 +199,7 @@ describe('Opening screen test cases', () => {
+
));
});
diff --git a/frontend/src/__test__/services/OpeningService.test.ts b/frontend/src/__test__/services/OpeningService.test.ts
index 665fc031..e3d050b2 100644
--- a/frontend/src/__test__/services/OpeningService.test.ts
+++ b/frontend/src/__test__/services/OpeningService.test.ts
@@ -1,7 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import axios from 'axios';
import {
- fetchRecentOpenings,
fetchOpeningsPerYear,
fetchFreeGrowingMilestones,
fetchRecentActions
@@ -21,57 +20,6 @@ describe('OpeningService', () => {
(getAuthIdToken as vi.Mock).mockReturnValue(authToken);
});
- describe('fetchRecentOpenings', () => {
- it('should fetch recent openings successfully', async () => {
- const mockData = {
- data: [
- {
- openingId: 1,
- forestFileId: '123',
- cuttingPermit: '456',
- timberMark: '789',
- cutBlock: 'A',
- grossAreaHa: 10,
- status: { description: 'Active' },
- category: { description: 'Category1' },
- disturbanceStart: '2023-01-01',
- entryTimestamp: '2023-01-01T00:00:00Z',
- updateTimestamp: '2023-01-02T00:00:00Z'
- }
- ]
- };
- (axios.get as vi.Mock).mockResolvedValue({ status: 200, data: mockData });
-
- const result = await fetchRecentOpenings();
-
- expect(axios.get).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent-openings?page=0&perPage=100`, {
- headers: { Authorization: `Bearer ${authToken}` }
- });
- expect(result).toEqual([
- {
- id: '1',
- openingId: '1',
- forestFileId: '123',
- cuttingPermit: '456',
- timberMark: '789',
- cutBlock: 'A',
- grossAreaHa: '10',
- status: 'Active',
- category: 'Category1',
- disturbanceStart: '2023-01-01',
- entryTimestamp: '2023-01-01',
- updateTimestamp: '2023-01-02'
- }
- ]);
- });
-
- it('should handle error while fetching recent openings', async () => {
- (axios.get as vi.Mock).mockRejectedValue(new Error('Network Error'));
-
- await expect(fetchRecentOpenings()).rejects.toThrow('Network Error');
- });
- });
-
describe('fetchOpeningsPerYear', () => {
it('should fetch openings per year successfully', async () => {
const mockData = [
diff --git a/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx b/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx
new file mode 100644
index 00000000..82d05cca
--- /dev/null
+++ b/frontend/src/__test__/services/queries/dashboard/dashboardQueries.test.tsx
@@ -0,0 +1,112 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import axios from "axios";
+import { usePostViewedOpening, postViewedOpening } from "../../../../services/queries/dashboard/dashboardQueries";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect } from "vitest";
+import { getAuthIdToken } from "../../../../services/AuthService";
+
+// Mock axios and the AuthService function
+vi.mock("axios");
+vi.mock("../../../../services/AuthService", () => ({
+ getAuthIdToken: vi.fn(),
+}));
+
+describe("postViewedOpening", () => {
+ const backendUrl = import.meta.env.VITE_BACKEND_URL;
+ const openingId = "123";
+
+ it("should send a PUT request and return data on success", async () => {
+ const mockResponse = { data: { message: "Success" } };
+ (axios.put as vi.Mock).mockResolvedValue(mockResponse);
+ (getAuthIdToken as vi.Mock).mockReturnValue("testAuthToken");
+
+ const result = await postViewedOpening(openingId);
+
+ expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent/${openingId}`, null, {
+ headers: { Authorization: `Bearer testAuthToken` },
+ });
+ expect(result).toEqual(mockResponse.data);
+ });
+
+ it("should throw a 403 error if the user is unauthorized", async () => {
+ (axios.put as vi.Mock).mockRejectedValue({ response: { status: 403 } });
+
+ await expect(postViewedOpening(openingId)).rejects.toThrow(
+ "Forbidden: You don't have permission to view this opening."
+ );
+ });
+
+ it("should throw a specific error message for other server errors", async () => {
+ const errorMessage = "Server error occurred";
+ (axios.put as vi.Mock).mockRejectedValue({
+ response: { data: { message: errorMessage } },
+ });
+
+ await expect(postViewedOpening(openingId)).rejects.toThrow(errorMessage);
+ });
+});
+
+describe("usePostViewedOpening", () => {
+ const queryClient = new QueryClient();
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+ );
+
+ // Mock Component to test the hook
+ const MockComponent = ({ openingId }: { openingId: string }) => {
+ const mutation = usePostViewedOpening();
+
+ return (
+
+ );
+ };
+
+ it("should call postViewedOpening when mutate is invoked", async () => {
+ const mockResponse = { message: "Success" };
+ const backendUrl = import.meta.env.VITE_BACKEND_URL;
+ (axios.put as vi.Mock).mockResolvedValue({ data: mockResponse });
+ (getAuthIdToken as vi.Mock).mockReturnValue("testAuthToken");
+
+ render(
+
+
+
+ );
+
+ // Trigger the mutation
+ screen.getByTestId("mutate-button").click();
+
+ // Wait for axios call
+ await waitFor(() =>
+ expect(axios.put).toHaveBeenCalledWith(`${backendUrl}/api/openings/recent/123`, null, {
+ headers: { Authorization: `Bearer testAuthToken` },
+ })
+ );
+ });
+
+ it("should handle errors when mutation fails", async () => {
+ const errorMessage = "Server error occurred";
+ (axios.put as vi.Mock).mockRejectedValue({
+ response: { data: { message: errorMessage } },
+ });
+
+ render(
+
+
+
+ );
+
+ // Trigger the mutation
+ screen.getByTestId("mutate-button").click();
+
+ await waitFor(() => {
+ expect(axios.put).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/frontend/src/__test__/services/search/openings.test.tsx b/frontend/src/__test__/services/search/openings.test.tsx
index 6ddbf33e..76a8c652 100644
--- a/frontend/src/__test__/services/search/openings.test.tsx
+++ b/frontend/src/__test__/services/search/openings.test.tsx
@@ -5,6 +5,7 @@ import { fetchOpenings, OpeningFilters } from "../../../services/search/openings
import { getAuthIdToken } from "../../../services/AuthService";
import { createDateParams } from "../../../utils/searchUtils";
import { describe, it, beforeEach, afterEach, vi, expect } from "vitest";
+import exp from "constants";
// Mock dependencies
vi.mock("axios");
@@ -53,10 +54,14 @@ const mockApiResponse = {
orgUnitCode: "DPG",
orgUnitName: "Prince George Natural Resource District",
entryUserId: "Datafix107808",
- statusCode: "APP",
- statusDescription: "Approved",
- categoryCode: "FTML",
- categoryDescription: "Forest Tenure - Major Licensee",
+ category: {
+ code: "CONT",
+ description: "SP as a part of contractual agreement",
+ },
+ status: {
+ code: "APP",
+ description: "Approved",
+ },
},
],
},
@@ -106,4 +111,29 @@ describe("fetchOpenings", () => {
await expect(fetchOpenings(sampleFilters)).rejects.toThrow("Network error");
});
+
+ it("should return flattened data structure with specific fields", async () => {
+ // Arrange: setting up the mock response for axios
+ mockedAxios.get.mockResolvedValue(mockApiResponse);
+
+ // Act: call the fetchOpenings function
+ const result = await fetchOpenings(sampleFilters);
+
+ // Assert: check that the response data is correctly flattened
+ const firstOpening = result.data[0];
+ expect(firstOpening.openingId).toEqual(9100129);
+ expect(firstOpening.categoryCode).toEqual("CONT");
+ expect(firstOpening.categoryDescription).toEqual("SP as a part of contractual agreement");
+ expect(firstOpening.statusCode).toEqual("APP");
+ expect(firstOpening.statusDescription).toEqual("Approved");
+ expect(firstOpening.timberMark).toEqual("W1729S");
+ expect(firstOpening.cutBlockId).toEqual("06-03");
+ expect(firstOpening.entryUserId).toEqual("Datafix107808");
+
+
+ // Confirm that original nested properties were removed
+ expect(firstOpening.status).toBeUndefined();
+ expect(firstOpening.category).toBeUndefined();
+ });
+
});
diff --git a/frontend/src/components/ActionButtons/index.tsx b/frontend/src/components/ActionButtons/index.tsx
new file mode 100644
index 00000000..877cfcc5
--- /dev/null
+++ b/frontend/src/components/ActionButtons/index.tsx
@@ -0,0 +1,32 @@
+// ActionButtons.tsx
+
+import React from "react";
+import { Button } from "@carbon/react";
+import * as Icons from "@carbon/icons-react";
+
+interface ActionButtonsProps {
+ rowId: string;
+}
+
+const ActionButtons: React.FC = ({ rowId }) => (
+ <>
+