From 639f3abbd2e68939276e2cf7b7c93e146f2a68f1 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 13:09:02 +0100 Subject: [PATCH 01/17] chore: Add @braintree/sanitize-url --- app/package.json | 1 + yarn.lock | 133 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 121 insertions(+), 13 deletions(-) diff --git a/app/package.json b/app/package.json index b5f9845f4..d17813812 100644 --- a/app/package.json +++ b/app/package.json @@ -16,6 +16,7 @@ "@apollo/client": "^3.3.20", "@apollo/server-plugin-landing-page-graphql-playground": "^4.0.1", "@babel/standalone": "^7.11.6", + "@braintree/sanitize-url": "^7.1.1", "@deck.gl/core": "^9.1.2", "@deck.gl/extensions": "^9.1.2", "@deck.gl/geo-layers": "^9.1.2", diff --git a/yarn.lock b/yarn.lock index 72fdcb035..a3983f333 100644 --- a/yarn.lock +++ b/yarn.lock @@ -311,7 +311,29 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.2.tgz#4183f9e642fd84e74e3eea7ffa93a412e3b102c9" integrity sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ== -"@babel/core@7.12.9", "@babel/core@^7.0.0", "@babel/core@^7.10.5", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.14.6", "@babel/core@^7.18.9", "@babel/core@^7.21.0", "@babel/core@^7.23.0", "@babel/core@^7.24.4", "@babel/core@^7.26.10", "@babel/core@^7.7.7": +"@babel/core@7.12.9": + version "7.12.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" + integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.5" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.7" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.9" + "@babel/types" "^7.12.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/core@^7.0.0", "@babel/core@^7.10.5", "@babel/core@^7.12.3", "@babel/core@^7.12.9", "@babel/core@^7.18.9", "@babel/core@^7.21.0", "@babel/core@^7.23.0", "@babel/core@^7.24.4", "@babel/core@^7.26.10", "@babel/core@^7.7.7": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.0.tgz#55dad808d5bf3445a108eefc88ea3fdf034749a4" integrity sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ== @@ -341,6 +363,17 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.5", "@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/generator@^7.21.1": version "7.28.3" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" @@ -705,6 +738,15 @@ "@babel/traverse" "^7.27.1" "@babel/types" "^7.27.1" +"@babel/helper-module-transforms@^7.12.1": + version "7.28.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" + "@babel/helper-module-transforms@^7.14.5": version "7.15.8" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz" @@ -977,6 +1019,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz" @@ -1006,6 +1053,14 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.22.19" +"@babel/helpers@^7.12.5": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + "@babel/helpers@^7.27.6": version "7.28.2" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.2.tgz#80f0918fecbfebea9af856c419763230040ee850" @@ -1051,13 +1106,25 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/parser@7.12.16", "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.6", "@babel/parser@^7.15.4", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.22.0", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0", "@babel/parser@^7.24.0", "@babel/parser@^7.24.4", "@babel/parser@^7.24.5", "@babel/parser@^7.25.9", "@babel/parser@^7.26.2", "@babel/parser@^7.27.0", "@babel/parser@^7.27.1", "@babel/parser@^7.27.2", "@babel/parser@^7.27.3", "@babel/parser@^7.28.0", "@babel/parser@^7.28.3": +"@babel/parser@7.12.16": + version "7.12.16" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.16.tgz#cc31257419d2c3189d394081635703f549fc1ed4" + integrity sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw== + +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.15.4", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@^7.22.0", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0", "@babel/parser@^7.24.0", "@babel/parser@^7.24.4", "@babel/parser@^7.24.5", "@babel/parser@^7.25.9", "@babel/parser@^7.26.2", "@babel/parser@^7.27.0", "@babel/parser@^7.27.1", "@babel/parser@^7.27.2", "@babel/parser@^7.27.3", "@babel/parser@^7.28.0": version "7.28.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e" integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g== dependencies: "@babel/types" "^7.28.0" +"@babel/parser@^7.12.7", "@babel/parser@^7.28.3", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== + dependencies: + "@babel/types" "^7.28.5" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.5": version "7.24.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz#4c3685eb9cd790bcad2843900fe0250c91ccf895" @@ -2247,6 +2314,15 @@ resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.26.10.tgz#1fce13e5a331a9dafff192b5db41350d72d44bed" integrity sha512-AYXK0hLWfEaK9WAePJqs30qro09a8w7X3YZzjukqtLXreE7xBZYdi5EMrP87T4UrVqmQ9tIX6L6SeTu5LDh3zw== +"@babel/template@^7.12.7", "@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + "@babel/template@^7.15.4": version "7.15.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz" @@ -2292,15 +2368,6 @@ "@babel/parser" "^7.27.0" "@babel/types" "^7.27.0" -"@babel/template@^7.27.1", "@babel/template@^7.27.2": - version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" - integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== - dependencies: - "@babel/code-frame" "^7.27.1" - "@babel/parser" "^7.27.2" - "@babel/types" "^7.27.1" - "@babel/traverse@7.12.13": version "7.12.13" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz" @@ -2331,6 +2398,19 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.12.9", "@babel/traverse@^7.28.3": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" + "@babel/traverse@^7.18.9": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" @@ -2445,6 +2525,14 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@babel/types@^7.12.7", "@babel/types@^7.28.4", "@babel/types@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/types@^7.16.7": version "7.17.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz" @@ -2562,6 +2650,11 @@ dependencies: buffer "^6.0.3" +"@braintree/sanitize-url@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz#15e19737d946559289b915e5dad3b4c28407735e" + integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw== + "@catalog/core@^4.0.1-canary.2": version "4.0.1-canary.2" resolved "https://registry.npmjs.org/@catalog/core/-/core-4.0.1-canary.2.tgz" @@ -15870,7 +15963,7 @@ fwd-stream@^1.0.4: dependencies: readable-stream "~1.0.26-4" -gensync@^1.0.0-beta.2: +gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -17362,7 +17455,7 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.0" -is-core-module@^2.15.1, is-core-module@^2.16.0: +is-core-module@^2.15.1, is-core-module@^2.16.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -23000,6 +23093,15 @@ resolve@^1.21.0, resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.3.2: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^2.0.0-next.5: version "2.0.0-next.5" resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" @@ -23353,6 +23455,11 @@ semver-compare@^1.0.0: resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^5.4.1: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" From fbc50b9229901a9a015182256003814625f59352 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 13:11:17 +0100 Subject: [PATCH 02/17] feat: Add links type to TableConfig --- app/charts/index.ts | 5 +++++ app/config-types.ts | 9 +++++++++ app/configurator/configurator-state/mocks.ts | 1 + app/docs/fixtures.ts | 1 + 4 files changed, 16 insertions(+) diff --git a/app/charts/index.ts b/app/charts/index.ts index f0e34d2f0..c28630ab6 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -617,6 +617,11 @@ export const getInitialConfig = ( showSearch: true, showAllRows: false, }, + links: { + enabled: false, + baseUrl: "", + componentId: "", + }, sorting: [], fields: Object.fromEntries( allDimensionsSorted.map((d, i) => [ diff --git a/app/config-types.ts b/app/config-types.ts index 7d705a9ca..f58fa635c 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -817,12 +817,20 @@ const TableColumn = t.type({ columnStyle: ColumnStyle, }); export type TableColumn = t.TypeOf; + const TableSettings = t.type({ showSearch: t.boolean, showAllRows: t.boolean, }); export type TableSettings = t.TypeOf; +const TableLinks = t.type({ + enabled: t.boolean, + baseUrl: t.string, + componentId: t.string, +}); +export type TableLinks = t.TypeOf; + const TableFields = t.record(t.string, TableColumn); export type TableFields = t.TypeOf; @@ -840,6 +848,7 @@ const TableConfig = t.intersection([ chartType: t.literal("table"), fields: TableFields, settings: TableSettings, + links: TableLinks, sorting: t.array(TableSortingOption), }, "TableConfig" diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 88887940d..499e6eb28 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -1299,6 +1299,7 @@ export const configJoinedCubes: Partial< conversionUnitsByComponentId: {}, chartType: "table", settings: { showSearch: true, showAllRows: false }, + links: { enabled: false, baseUrl: "", componentId: "" }, sorting: [], fields: { joinBy__0: { diff --git a/app/docs/fixtures.ts b/app/docs/fixtures.ts index dd273e52e..0c3d7f4cf 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -1143,6 +1143,7 @@ export const tableConfig: TableConfig = { }, }, settings: { showSearch: true, showAllRows: true }, + links: { enabled: false, baseUrl: "", componentId: "" }, sorting: [ { componentId: From a279ac84ab372370ea4cf5a72c75a473a4e7b125 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 13:12:29 +0100 Subject: [PATCH 03/17] feat: Add getLinkHref --- app/charts/table/table-helpers.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/charts/table/table-helpers.ts diff --git a/app/charts/table/table-helpers.ts b/app/charts/table/table-helpers.ts new file mode 100644 index 000000000..f7bb20168 --- /dev/null +++ b/app/charts/table/table-helpers.ts @@ -0,0 +1,16 @@ +import { Cell } from "react-table"; + +import { getSlugifiedId } from "@/charts/shared/chart-helpers"; +import { ColumnMeta } from "@/charts/table/table-state"; +import { Observation } from "@/domain/data"; + +export const getLinkHref = ( + cell: Cell, + { baseUrl, linkComponentId: id }: Extract +) => { + const { original } = cell.row; + const value = + original[getSlugifiedId(`${id}/__iri__`)] ?? original[getSlugifiedId(id)]; + + return value ? `${baseUrl}${value}` : ""; +}; From 365e72ba6d43814769cab1794bceb733fd3f1eca Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 13:20:18 +0100 Subject: [PATCH 04/17] refactor: Pass defaultValue --- app/components/form.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/form.tsx b/app/components/form.tsx index 40b3caa7f..7d04c3869 100644 --- a/app/components/form.tsx +++ b/app/components/form.tsx @@ -302,6 +302,7 @@ export const Select = ({ variant, size = "md", value, + defaultValue, disabled, options, optionGroups, @@ -369,6 +370,7 @@ export const Select = ({ name={id} onChange={onChange} value={value} + defaultValue={defaultValue} disabled={disabled} open={open} onOpen={handleOpen} From 1e0ea6f289e2279ec08376dbfa069046485fb17f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 13:33:43 +0100 Subject: [PATCH 05/17] feat: Add TableLinkCell --- app/charts/table/table-helpers.ts | 16 ------------- app/charts/table/table-helpers.tsx | 36 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 16 deletions(-) delete mode 100644 app/charts/table/table-helpers.ts create mode 100644 app/charts/table/table-helpers.tsx diff --git a/app/charts/table/table-helpers.ts b/app/charts/table/table-helpers.ts deleted file mode 100644 index f7bb20168..000000000 --- a/app/charts/table/table-helpers.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Cell } from "react-table"; - -import { getSlugifiedId } from "@/charts/shared/chart-helpers"; -import { ColumnMeta } from "@/charts/table/table-state"; -import { Observation } from "@/domain/data"; - -export const getLinkHref = ( - cell: Cell, - { baseUrl, linkComponentId: id }: Extract -) => { - const { original } = cell.row; - const value = - original[getSlugifiedId(`${id}/__iri__`)] ?? original[getSlugifiedId(id)]; - - return value ? `${baseUrl}${value}` : ""; -}; diff --git a/app/charts/table/table-helpers.tsx b/app/charts/table/table-helpers.tsx new file mode 100644 index 000000000..3d0eb85f0 --- /dev/null +++ b/app/charts/table/table-helpers.tsx @@ -0,0 +1,36 @@ +import { IconButton } from "@mui/material"; +import { Cell } from "react-table"; + +import { getSlugifiedId } from "@/charts/shared/chart-helpers"; +import { ColumnMeta } from "@/charts/table/table-state"; +import { Flex } from "@/components/flex"; +import { Observation } from "@/domain/data"; +import { Icon } from "@/icons"; + +export const getLinkHref = ( + cell: Cell, + { baseUrl, linkComponentId: id }: Extract +) => { + const { original } = cell.row; + const value = + original[getSlugifiedId(`${id}/__iri__`)] ?? original[getSlugifiedId(id)]; + + return value ? `${baseUrl}${value}` : ""; +}; + +export const TableLinkCell = ({ href }: { href: string }) => { + return ( + + + + + + ); +}; From 57905c8ed0592d7aebf27bc453604cf2b6832b8d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 13:35:01 +0100 Subject: [PATCH 06/17] feat: Render link column when needed --- app/charts/table/cell-desktop.tsx | 4 ++ app/charts/table/cell-mobile.tsx | 5 +++ app/charts/table/table-content.tsx | 45 +++++++++++++------- app/charts/table/table-state.tsx | 67 +++++++++++++++++++++++++++--- 4 files changed, 100 insertions(+), 21 deletions(-) diff --git a/app/charts/table/cell-desktop.tsx b/app/charts/table/cell-desktop.tsx index a93569fbe..176a6f5c9 100644 --- a/app/charts/table/cell-desktop.tsx +++ b/app/charts/table/cell-desktop.tsx @@ -5,6 +5,7 @@ import { ScaleLinear } from "d3-scale"; import { Cell } from "react-table"; import { BAR_CELL_PADDING } from "@/charts/table/constants"; +import { getLinkHref, TableLinkCell } from "@/charts/table/table-helpers"; import { ColumnMeta } from "@/charts/table/table-state"; import { Tag } from "@/charts/table/tag"; import { Flex } from "@/components/flex"; @@ -160,6 +161,9 @@ export const CellDesktop = ({ )} ); + case "link": + const linkHref = getLinkHref(cell, columnMeta); + return ; default: return ( ); } + case "link": { + const linkHref = getLinkHref(cell, columnMeta); + return ; + } default: { return ( diff --git a/app/charts/table/table-content.tsx b/app/charts/table/table-content.tsx index 864a75d84..1d9142b06 100644 --- a/app/charts/table/table-content.tsx +++ b/app/charts/table/table-content.tsx @@ -91,6 +91,17 @@ export const TableContent = ({ children }: { children: ReactNode }) => { tableColumnsMeta[column.id]; // We assume that the customSortCount items are at the beginning of the sorted array, so any item with a lower index must be a custom sorted one const isCustomSorted = column.sortedIndex < customSortCount; + const isLinkColumn = + tableColumnsMeta[column.id].type === "link"; + const isSortable = !isLinkColumn; + + const BaseHeader = ( + + + {column.render("Header")} + + + ); return ( // eslint-disable-next-line react/jsx-key @@ -101,23 +112,25 @@ export const TableContent = ({ children }: { children: ReactNode }) => { ? classes.headerGroupMeasure : undefined )} - {...column.getHeaderProps(column.getSortByToggleProps())} + {...column.getHeaderProps( + isSortable ? column.getSortByToggleProps() : undefined + )} > - - - - {column.render("Header")} - - - + {isSortable ? ( + + {BaseHeader} + + ) : ( + BaseHeader + )} ); })} diff --git a/app/charts/table/table-state.tsx b/app/charts/table/table-state.tsx index 7bc7c1322..2b6be8a9d 100644 --- a/app/charts/table/table-state.tsx +++ b/app/charts/table/table-state.tsx @@ -94,11 +94,18 @@ type TextColumnMeta = MKColumnMeta<{ type: "text"; }>; +type LinkColumnMeta = MKColumnMeta<{ + type: "link"; + baseUrl: string; + linkComponentId: string; +}>; + export type ColumnMeta = | BarColumnMeta | HeatmapColumnMeta | CategoryColumnMeta | TextColumnMeta + | LinkColumnMeta | MKColumnMeta<{ type: "other" }>; type TableInteractiveTimeRangeState = { @@ -127,9 +134,15 @@ const useTableState = ( const { chartConfig, dimensions, measures } = chartProps; const { getX } = variables; const { chartData, allData, timeRangeData } = data; - const { fields, settings, sorting } = chartConfig; + const { fields, settings, links, sorting } = chartConfig; const formatNumber = useFormatNumber(); + const linkColumnId = "__link__"; + const hasLinkColumn = + links.enabled && + links.baseUrl.trim() !== "" && + links.componentId.trim() !== ""; + const hasBar = Object.values(fields).some( (fValue) => fValue.columnStyle.type === "bar" ); @@ -212,7 +225,7 @@ const useTableState = ( const tableColumns = useMemo(() => { const allComponents = [...dimensions, ...measures]; - return orderedTableColumns.map((c) => { + const columns = orderedTableColumns.map((c) => { const headerComponent = allComponents.find((d) => d.id === c.componentId); if (!headerComponent) { @@ -270,7 +283,25 @@ const useTableState = ( }, }; }); - }, [orderedTableColumns, chartData, dimensions, measures, formatNumber]); + + if (hasLinkColumn) { + columns.push({ + Header: "", + accessor: linkColumnId, + width: 60, + sortType: () => 0, + }); + } + + return columns; + }, [ + dimensions, + measures, + orderedTableColumns, + hasLinkColumn, + chartData, + formatNumber, + ]); // Groupings used by react-table const groupingIds = useMemo( @@ -317,7 +348,7 @@ const useTableState = ( const allColumnsById = Object.fromEntries( [...dimensions, ...measures].map((x) => [x.id, x]) ); - return mapKeys( + const meta: Record = mapKeys( mapValues(fields, (columnMeta, id) => { const slugifiedId = getSlugifiedId(id); const columnStyle = columnMeta.columnStyle; @@ -406,7 +437,33 @@ const useTableState = ( }), (v) => v.slugifiedId ); - }, [chartData, dimensions, fields, formatters, measures]); + + if (hasLinkColumn) { + const linkComponent = allColumnsById[links.componentId]; + const linkColumnMeta: LinkColumnMeta = { + type: "link", + dim: linkComponent, + id: linkColumnId, + slugifiedId: linkColumnId, + columnComponentType: linkComponent.__typename, + formatter: () => "", + baseUrl: links.baseUrl, + linkComponentId: getSlugifiedId(links.componentId), + }; + meta[linkColumnId] = linkColumnMeta; + } + + return meta; + }, [ + dimensions, + measures, + fields, + hasLinkColumn, + formatters, + chartData, + links.componentId, + links.baseUrl, + ]); const xScaleTimeRange = useMemo(() => { const xScaleTimeRangeDomain = extent(timeRangeData, (d) => getX(d)) as [ From 0ae4559f00b9761b6a069800e338ab337f7d37b3 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 14:08:48 +0100 Subject: [PATCH 07/17] feat: Add TableLinksSection --- .../table/configurator/links-section.tsx | 183 ++++++++++++++++++ .../table/table-chart-configurator.tsx | 2 + app/locales/de/messages.po | 20 ++ app/locales/en/messages.po | 20 ++ app/locales/fr/messages.po | 20 ++ app/locales/it/messages.po | 20 ++ 6 files changed, 265 insertions(+) create mode 100644 app/configurator/table/configurator/links-section.tsx diff --git a/app/configurator/table/configurator/links-section.tsx b/app/configurator/table/configurator/links-section.tsx new file mode 100644 index 000000000..95d9ef980 --- /dev/null +++ b/app/configurator/table/configurator/links-section.tsx @@ -0,0 +1,183 @@ +import { sanitizeUrl } from "@braintree/sanitize-url"; +import { t, Trans } from "@lingui/macro"; +import { SelectChangeEvent } from "@mui/material"; +import { KeyboardEvent, useEffect, useMemo, useState } from "react"; + +import { Input, Select } from "@/components/form"; +import { TableConfig } from "@/config-types"; +import { + ControlSection, + ControlSectionContent, + SectionTitle, +} from "@/configurator/components/chart-controls/section"; +import { ChartOptionCheckboxField } from "@/configurator/components/field"; +import { + isConfiguring, + useConfiguratorState, +} from "@/configurator/configurator-state"; +import { Dimension } from "@/domain/data"; +import { useLocale } from "@/locales/use-locale"; +import { useEvent } from "@/utils/use-event"; + +export const TableLinksSection = ({ + chartConfig, + dimensions, +}: { + chartConfig: TableConfig; + dimensions: Dimension[]; +}) => { + const locale = useLocale(); + const [_, dispatch] = useConfiguratorState(isConfiguring); + + const dimensionOptions = useMemo(() => { + return dimensions.map((d) => ({ + value: d.id, + label: d.label, + })); + }, [dimensions]); + + const handleLinkComponentIdChange = useEvent( + (e: SelectChangeEvent) => { + const linkComponentId = e.target.value as string; + dispatch({ + type: "CHART_FIELD_UPDATED", + value: { + locale, + field: null, + path: "links.componentId", + value: linkComponentId, + }, + }); + } + ); + + return ( + + + Links + + + + + setInputValue(e.target.value)} + onBlur={handleCommit} + onKeyDown={handleKeyDown} + /> + ); +}; + +const normalizeUrl = (url: URL) => { + if (!url.pathname.endsWith("/")) { + url.pathname = url.pathname + "/"; + } + + return url.toString(); +}; diff --git a/app/configurator/table/table-chart-configurator.tsx b/app/configurator/table/table-chart-configurator.tsx index 05f00bdd7..3770ab1b1 100644 --- a/app/configurator/table/table-chart-configurator.tsx +++ b/app/configurator/table/table-chart-configurator.tsx @@ -19,6 +19,7 @@ import { ChartOptionCheckboxField, } from "@/configurator/components/field"; import { useOrderedTableColumns } from "@/configurator/components/ui-helpers"; +import { TableLinksSection } from "@/configurator/table/configurator/links-section"; import { useTableChartController } from "@/configurator/table/table-chart-configurator.hook"; const useStyles = makeStyles((theme: Theme) => ({ @@ -152,6 +153,7 @@ export const ChartConfiguratorTable = ({ items={columnFields} /> + ); }; diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po index bdec3b4c1..172cad7ac 100644 --- a/app/locales/de/messages.po +++ b/app/locales/de/messages.po @@ -388,6 +388,10 @@ msgstr "Basis Farbe" msgid "chart.map.layers.base" msgstr "Basiskarte" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrl" +msgstr "Basis-URL" + #: app/configurator/components/custom-layers-selector.tsx msgid "chart.map.layers.base.behind-area-layer" msgstr "Hinter der Flächenebene" @@ -743,6 +747,10 @@ msgstr "Beschreibung hinzufügen" msgid "controls.metadata-panel.metadata" msgstr "Details" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Dimension" + #: app/configurator/components/chart-options-selector/color-component-field.tsx msgid "controls.color.scale.type.discrete" msgstr "Diskret" @@ -1129,6 +1137,10 @@ msgstr "Lineare Interpolation" msgid "controls.chart.type.line" msgstr "Linien" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.section.links" +msgstr "Links" + #: app/components/chart-filters-list.tsx #: app/components/hint.tsx #: app/components/hint.tsx @@ -1400,6 +1412,10 @@ msgstr "Bitte geben Sie eine gültige Nummer ein" msgid "controls.adjust-scale-domain.invalid-number-error" msgstr "Bitte geben Sie eine gültige Nummer ein" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrlInvalid" +msgstr "Bitte geben Sie eine gültige URL ein" + #: app/components/hint.tsx msgid "hint.nodata.message" msgstr "Bitte versuchen Sie es mit einer anderen Filterkombination." @@ -1740,6 +1756,10 @@ msgstr "Legendentitel anzeigen" msgid "show.less" msgstr "Weniger anzeigen" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.showLinks" +msgstr "Linkspalte anzeigen" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Zeig mehr" diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po index 01b1bf31b..3de8840d9 100644 --- a/app/locales/en/messages.po +++ b/app/locales/en/messages.po @@ -388,6 +388,10 @@ msgstr "Base color" msgid "chart.map.layers.base" msgstr "Base map" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrl" +msgstr "Base URL" + #: app/configurator/components/custom-layers-selector.tsx msgid "chart.map.layers.base.behind-area-layer" msgstr "Behind area layer" @@ -743,6 +747,10 @@ msgstr "Description" msgid "controls.metadata-panel.metadata" msgstr "Details" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Dimension" + #: app/configurator/components/chart-options-selector/color-component-field.tsx msgid "controls.color.scale.type.discrete" msgstr "Discrete" @@ -1129,6 +1137,10 @@ msgstr "Linear interpolation" msgid "controls.chart.type.line" msgstr "Lines" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.section.links" +msgstr "Links" + #: app/components/chart-filters-list.tsx #: app/components/hint.tsx #: app/components/hint.tsx @@ -1400,6 +1412,10 @@ msgstr "Please enter a valid number" msgid "controls.adjust-scale-domain.invalid-number-error" msgstr "Please enter a valid number" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrlInvalid" +msgstr "Please enter a valid URL" + #: app/components/hint.tsx msgid "hint.nodata.message" msgstr "Please try with another combination of filters." @@ -1740,6 +1756,10 @@ msgstr "Show legend titles" msgid "show.less" msgstr "Show less" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.showLinks" +msgstr "Show link column" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Show more" diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po index 30cbfc895..9c158c7a7 100644 --- a/app/locales/fr/messages.po +++ b/app/locales/fr/messages.po @@ -388,6 +388,10 @@ msgstr "Couleur de base" msgid "chart.map.layers.base" msgstr "Carte de base" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrl" +msgstr "URL de base" + #: app/configurator/components/custom-layers-selector.tsx msgid "chart.map.layers.base.behind-area-layer" msgstr "Derrière la couche de surface" @@ -743,6 +747,10 @@ msgstr "Description" msgid "controls.metadata-panel.metadata" msgstr "Détails" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Dimension" + #: app/configurator/components/chart-options-selector/color-component-field.tsx msgid "controls.color.scale.type.discrete" msgstr "Discrète" @@ -1129,6 +1137,10 @@ msgstr "Interpolation linéaire" msgid "controls.chart.type.line" msgstr "Lignes" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.section.links" +msgstr "Links" + #: app/components/chart-filters-list.tsx #: app/components/hint.tsx #: app/components/hint.tsx @@ -1400,6 +1412,10 @@ msgstr "Veuillez entrer un numéro valide" msgid "controls.adjust-scale-domain.invalid-number-error" msgstr "Veuillez entrer un numéro valide" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrlInvalid" +msgstr "Veuillez saisir une URL valide" + #: app/components/hint.tsx msgid "hint.nodata.message" msgstr "Veuillez essayer avec une autre combinaison de filtres." @@ -1740,6 +1756,10 @@ msgstr "Afficher les titres des légendes" msgid "show.less" msgstr "Afficher moins" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.showLinks" +msgstr "Afficher la colonne des liens" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Détails" diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po index e2c5e1c3c..f78193d60 100644 --- a/app/locales/it/messages.po +++ b/app/locales/it/messages.po @@ -388,6 +388,10 @@ msgstr "Colore base" msgid "chart.map.layers.base" msgstr "Carta di base" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrl" +msgstr "URL di base" + #: app/configurator/components/custom-layers-selector.tsx msgid "chart.map.layers.base.behind-area-layer" msgstr "Dietro lo strato dell'area" @@ -743,6 +747,10 @@ msgstr "Descrizione" msgid "controls.metadata-panel.metadata" msgstr "Dettagli" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Dimensione" + #: app/configurator/components/chart-options-selector/color-component-field.tsx msgid "controls.color.scale.type.discrete" msgstr "Discreto" @@ -1129,6 +1137,10 @@ msgstr "Interpolazione lineare" msgid "controls.chart.type.line" msgstr "Linee" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.section.links" +msgstr "Collegamenti" + #: app/components/chart-filters-list.tsx #: app/components/hint.tsx #: app/components/hint.tsx @@ -1400,6 +1412,10 @@ msgstr "Inserisci un numero valido" msgid "controls.adjust-scale-domain.invalid-number-error" msgstr "Inserisci un numero valido" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.baseUrlInvalid" +msgstr "Inserisci un URL valido" + #: app/components/hint.tsx msgid "hint.nodata.message" msgstr "Prova con un'altra combinazione di filtri." @@ -1740,6 +1756,10 @@ msgstr "Mostra i titoli delle leggende" msgid "show.less" msgstr "Mostra meno" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.showLinks" +msgstr "Mostra colonna link" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Mostra di più" From f50a3603e4db1a2debb2bf94d650714b4501278f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 14:09:17 +0100 Subject: [PATCH 08/17] docs: Update CHANGELOG --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6f5dbb35..9e83c1aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,15 @@ You can also check the ## Unreleased -- Fix +- Features + - Added a way to create a link column in table charts +- Fixes - Clicking on Y axis title in bar charts now open Data tab in the details panels, as in other chart types ### 6.1.2 - 2025-10-22 -- Fix +- Fixes - Correctly typed DataSourceUrl variable when querying version history of a cube From 884b8c0dbf009ee0c1239aae8b074e5be9c4bc6a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 14:28:15 +0100 Subject: [PATCH 09/17] feat: Add chart config migrations --- app/utils/chart-config/constants.ts | 4 +-- app/utils/chart-config/versioning.ts | 39 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/app/utils/chart-config/constants.ts b/app/utils/chart-config/constants.ts index c699b462f..7837e2a31 100644 --- a/app/utils/chart-config/constants.ts +++ b/app/utils/chart-config/constants.ts @@ -1,3 +1,3 @@ -export const CONFIGURATOR_STATE_VERSION = "5.0.0"; +export const CONFIGURATOR_STATE_VERSION = "5.1.0"; -export const CHART_CONFIG_VERSION = "5.0.0"; +export const CHART_CONFIG_VERSION = "5.1.0"; diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 5caf7b9e7..3f5baa974 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1638,6 +1638,39 @@ export const chartConfigMigrations: Migration[] = [ const newConfig = { ...config, version: "4.5.0" }; delete newConfig.annotations; + return newConfig; + }, + }, + { + from: "5.0.0", + to: "5.1.0", + description: `table chart { + links { + + enabled + + baseUrl + + componentId + } + }`, + up: (config) => { + const newConfig = { ...config, version: "5.1.0" }; + + if (newConfig.chartType === "table") { + newConfig.links = { + enabled: false, + baseUrl: "", + componentId: "", + }; + } + + return newConfig; + }, + down: (config) => { + const newConfig = { ...config, version: "5.0.0" }; + + if (newConfig.chartType === "table") { + delete newConfig.links; + } + return newConfig; }, }, @@ -2272,6 +2305,12 @@ export const configuratorStateMigrations: Migration[] = [ fromChartConfigVersion: "4.6.0", toChartConfigVersion: "5.0.0", }), + makeBumpChartConfigVersionMigration({ + fromVersion: "5.0.0", + toVersion: "5.1.0", + fromChartConfigVersion: "5.0.0", + toChartConfigVersion: "5.1.0", + }), ]; export const migrateConfiguratorState = makeMigrate( From 3984143f85b0e511e0e181d0f119fc33d5178a48 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Tue, 4 Nov 2025 14:32:23 +0100 Subject: [PATCH 10/17] fix: Test --- app/configurator/configurator-state/reducer.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/configurator/configurator-state/reducer.spec.tsx b/app/configurator/configurator-state/reducer.spec.tsx index 8614ed533..c282d894e 100644 --- a/app/configurator/configurator-state/reducer.spec.tsx +++ b/app/configurator/configurator-state/reducer.spec.tsx @@ -606,7 +606,7 @@ describe("deriveFiltersFromFields", () => { "it": "", }, }, - "version": "5.0.0", + "version": "5.1.0", } `); }); From dbb782714ae0dbdb8605d39b07a356bc4af156d2 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Wed, 5 Nov 2025 09:51:38 +0100 Subject: [PATCH 11/17] feat: Render links directly in respective column --- app/charts/table/cell-desktop.tsx | 91 +++++++------ app/charts/table/cell-mobile.tsx | 164 ++++++++++++----------- app/charts/table/linked-cell-wrapper.tsx | 74 ++++++++++ app/charts/table/table-content.tsx | 45 +++---- app/charts/table/table-helpers.tsx | 36 ----- app/charts/table/table-state.tsx | 60 +-------- 6 files changed, 232 insertions(+), 238 deletions(-) create mode 100644 app/charts/table/linked-cell-wrapper.tsx delete mode 100644 app/charts/table/table-helpers.tsx diff --git a/app/charts/table/cell-desktop.tsx b/app/charts/table/cell-desktop.tsx index 176a6f5c9..da9eb11e4 100644 --- a/app/charts/table/cell-desktop.tsx +++ b/app/charts/table/cell-desktop.tsx @@ -4,9 +4,10 @@ import { hcl } from "d3-color"; import { ScaleLinear } from "d3-scale"; import { Cell } from "react-table"; +import { useChartState } from "@/charts/shared/chart-state"; import { BAR_CELL_PADDING } from "@/charts/table/constants"; -import { getLinkHref, TableLinkCell } from "@/charts/table/table-helpers"; -import { ColumnMeta } from "@/charts/table/table-state"; +import { LinkedCellWrapper } from "@/charts/table/linked-cell-wrapper"; +import { ColumnMeta, TableChartState } from "@/charts/table/table-state"; import { Tag } from "@/charts/table/tag"; import { Flex } from "@/components/flex"; import { Observation } from "@/domain/data"; @@ -60,6 +61,7 @@ export const CellDesktop = ({ barShowBackground, } = columnMeta; const classes = useStyles(); + const { links } = useChartState() as TableChartState; switch (columnMeta.type) { case "text": @@ -78,7 +80,9 @@ export const CellDesktop = ({ }} {...cell.getCellProps()} > - {columnMeta.formatter(cell)} + + {columnMeta.formatter(cell)} + ); case "category": @@ -88,9 +92,11 @@ export const CellDesktop = ({ sx={{ alignItems: "center", fontWeight: textStyle, pl: 1, pr: 3 }} {...cell.getCellProps()} > - - {columnMeta.formatter(cell)} - + + + {columnMeta.formatter(cell)} + + ); case "heatmap": @@ -110,7 +116,9 @@ export const CellDesktop = ({ }} {...cell.getCellProps()} > - {columnMeta.formatter(cell)} + + {columnMeta.formatter(cell)} + ); case "bar": @@ -126,44 +134,43 @@ export const CellDesktop = ({ }} {...cell.getCellProps()} > - {columnMeta.formatter(cell)} - {cell.value !== null && widthScale && ( - + + {columnMeta.formatter(cell)} + {cell.value !== null && widthScale && ( 0 ? barColorPositive : barColorNegative, + width: widthScale.range()[1], + height: 18, + position: "relative", + backgroundColor: barShowBackground + ? barColorBackground + : "grey.100", }} - /> - - - )} + > + 0 ? barColorPositive : barColorNegative, + }} + /> + + + )} + ); - case "link": - const linkHref = getLinkHref(cell, columnMeta); - return ; default: return ( - {columnMeta.formatter(cell)} + + {columnMeta.formatter(cell)} + ); } diff --git a/app/charts/table/cell-mobile.tsx b/app/charts/table/cell-mobile.tsx index c78031ec7..cad9dc1e2 100644 --- a/app/charts/table/cell-mobile.tsx +++ b/app/charts/table/cell-mobile.tsx @@ -4,8 +4,8 @@ import { Cell } from "react-table"; import { useChartState } from "@/charts/shared/chart-state"; import { getBarLeftOffset, getBarWidth } from "@/charts/table/cell-desktop"; -import { getLinkHref, TableLinkCell } from "@/charts/table/table-helpers"; -import { ColumnMeta } from "@/charts/table/table-state"; +import { LinkedCellWrapper } from "@/charts/table/linked-cell-wrapper"; +import { ColumnMeta, TableChartState } from "@/charts/table/table-state"; import { Tag } from "@/charts/table/tag"; import { Flex } from "@/components/flex"; import { Observation } from "@/domain/data"; @@ -18,7 +18,8 @@ export const DDContent = ({ cell: Cell; columnMeta: ColumnMeta; }) => { - const { bounds } = useChartState(); + const chartState = useChartState() as TableChartState; + const { bounds, links } = chartState; const { chartWidth } = bounds; const formatNumber = useFormatNumber(); @@ -44,40 +45,46 @@ export const DDContent = ({ fontWeight: textStyle, }} > - {columnComponentType === "NumericalMeasure" - ? formatNumber(cell.value) - : cell.render("Cell")} + + {columnComponentType === "NumericalMeasure" + ? formatNumber(cell.value) + : cell.render("Cell")} + ); } case "category": { const { colorScale } = columnMeta; return ( - - {cell.render("Cell")} - + + + {cell.render("Cell")} + + ); } case "heatmap": { const { colorScale } = columnMeta; const isNull = cell.value === null; return ( - - {formatNumber(cell.value)} - + + + {formatNumber(cell.value)} + + ); } case "bar": { @@ -85,66 +92,69 @@ export const DDContent = ({ // Reset width scale range based on current viewport widthScale.range([0, chartWidth / 2]); return ( - - {formatNumber(cell.value)} - {cell.value !== null && ( - + + + {formatNumber(cell.value)} + {cell.value !== null && ( 0 ? barColorPositive : barColorNegative, + backgroundColor: barShowBackground + ? barColorBackground + : "grey.100", }} - /> - - - )} - + > + 0 ? barColorPositive : barColorNegative, + }} + /> + + + )} + + ); } - case "link": { - const linkHref = getLinkHref(cell, columnMeta); - return ; - } default: { return ( - - {columnComponentType === "NumericalMeasure" - ? formatNumber(cell.value) - : cell.render("Cell")} - + + + {columnComponentType === "NumericalMeasure" + ? formatNumber(cell.value) + : cell.render("Cell")} + + ); } } diff --git a/app/charts/table/linked-cell-wrapper.tsx b/app/charts/table/linked-cell-wrapper.tsx new file mode 100644 index 000000000..a22b1022f --- /dev/null +++ b/app/charts/table/linked-cell-wrapper.tsx @@ -0,0 +1,74 @@ +import { Link, Theme } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { ReactNode } from "react"; +import { Cell } from "react-table"; + +import { getSlugifiedId } from "@/charts/shared/chart-helpers"; +import { ColumnMeta } from "@/charts/table/table-state"; +import { TableLinks } from "@/config-types"; +import { Observation } from "@/domain/data"; +import { Icon } from "@/icons"; + +const useStyles = makeStyles((theme: Theme) => ({ + link: { + display: "inline-flex", + alignItems: "center", + gap: theme.spacing(1), + color: "inherit", + textDecoration: "none", + "&:hover": { + textDecoration: "underline", + }, + }, +})); + +const getLinkHref = ( + cell: Cell, + baseUrl: string, + linkComponentId: string +): string => { + const slugifiedId = getSlugifiedId(linkComponentId); + const { original } = cell.row; + const value = + original[getSlugifiedId(`${linkComponentId}/__iri__`)] ?? + original[slugifiedId]; + + return value ? `${baseUrl}${encodeURIComponent(value)}` : ""; +}; + +export const LinkedCellWrapper = ({ + children, + cell, + columnMeta, + links, +}: { + children: ReactNode; + cell: Cell; + columnMeta: ColumnMeta; + links: TableLinks; +}) => { + const classes = useStyles(); + const isLinkedColumn = + links.enabled && + links.baseUrl.trim() !== "" && + links.componentId.trim() !== "" && + getSlugifiedId(links.componentId) === columnMeta.slugifiedId; + const href = getLinkHref(cell, links.baseUrl, links.componentId); + + if (!isLinkedColumn || !href) { + return <>{children}; + } + + return ( + + {children} + + + ); +}; diff --git a/app/charts/table/table-content.tsx b/app/charts/table/table-content.tsx index 1d9142b06..864a75d84 100644 --- a/app/charts/table/table-content.tsx +++ b/app/charts/table/table-content.tsx @@ -91,17 +91,6 @@ export const TableContent = ({ children }: { children: ReactNode }) => { tableColumnsMeta[column.id]; // We assume that the customSortCount items are at the beginning of the sorted array, so any item with a lower index must be a custom sorted one const isCustomSorted = column.sortedIndex < customSortCount; - const isLinkColumn = - tableColumnsMeta[column.id].type === "link"; - const isSortable = !isLinkColumn; - - const BaseHeader = ( - - - {column.render("Header")} - - - ); return ( // eslint-disable-next-line react/jsx-key @@ -112,25 +101,23 @@ export const TableContent = ({ children }: { children: ReactNode }) => { ? classes.headerGroupMeasure : undefined )} - {...column.getHeaderProps( - isSortable ? column.getSortByToggleProps() : undefined - )} + {...column.getHeaderProps(column.getSortByToggleProps())} > - {isSortable ? ( - - {BaseHeader} - - ) : ( - BaseHeader - )} + + + + {column.render("Header")} + + + ); })} diff --git a/app/charts/table/table-helpers.tsx b/app/charts/table/table-helpers.tsx deleted file mode 100644 index 3d0eb85f0..000000000 --- a/app/charts/table/table-helpers.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { IconButton } from "@mui/material"; -import { Cell } from "react-table"; - -import { getSlugifiedId } from "@/charts/shared/chart-helpers"; -import { ColumnMeta } from "@/charts/table/table-state"; -import { Flex } from "@/components/flex"; -import { Observation } from "@/domain/data"; -import { Icon } from "@/icons"; - -export const getLinkHref = ( - cell: Cell, - { baseUrl, linkComponentId: id }: Extract -) => { - const { original } = cell.row; - const value = - original[getSlugifiedId(`${id}/__iri__`)] ?? original[getSlugifiedId(id)]; - - return value ? `${baseUrl}${value}` : ""; -}; - -export const TableLinkCell = ({ href }: { href: string }) => { - return ( - - - - - - ); -}; diff --git a/app/charts/table/table-state.tsx b/app/charts/table/table-state.tsx index 2b6be8a9d..ef797170a 100644 --- a/app/charts/table/table-state.tsx +++ b/app/charts/table/table-state.tsx @@ -38,6 +38,7 @@ import { ColumnStyleHeatmap, ComponentType, TableConfig, + TableLinks, } from "@/configurator"; import { mkNumber, @@ -94,18 +95,11 @@ type TextColumnMeta = MKColumnMeta<{ type: "text"; }>; -type LinkColumnMeta = MKColumnMeta<{ - type: "link"; - baseUrl: string; - linkComponentId: string; -}>; - export type ColumnMeta = | BarColumnMeta | HeatmapColumnMeta | CategoryColumnMeta | TextColumnMeta - | LinkColumnMeta | MKColumnMeta<{ type: "other" }>; type TableInteractiveTimeRangeState = { @@ -124,6 +118,7 @@ export type TableChartState = CommonChartState & groupingIds: string[]; hiddenIds: string[]; sortingIds: { id: string; desc: boolean }[]; + links: TableLinks; }; const useTableState = ( @@ -137,12 +132,6 @@ const useTableState = ( const { fields, settings, links, sorting } = chartConfig; const formatNumber = useFormatNumber(); - const linkColumnId = "__link__"; - const hasLinkColumn = - links.enabled && - links.baseUrl.trim() !== "" && - links.componentId.trim() !== ""; - const hasBar = Object.values(fields).some( (fValue) => fValue.columnStyle.type === "bar" ); @@ -284,24 +273,8 @@ const useTableState = ( }; }); - if (hasLinkColumn) { - columns.push({ - Header: "", - accessor: linkColumnId, - width: 60, - sortType: () => 0, - }); - } - return columns; - }, [ - dimensions, - measures, - orderedTableColumns, - hasLinkColumn, - chartData, - formatNumber, - ]); + }, [dimensions, measures, orderedTableColumns, chartData, formatNumber]); // Groupings used by react-table const groupingIds = useMemo( @@ -438,32 +411,8 @@ const useTableState = ( (v) => v.slugifiedId ); - if (hasLinkColumn) { - const linkComponent = allColumnsById[links.componentId]; - const linkColumnMeta: LinkColumnMeta = { - type: "link", - dim: linkComponent, - id: linkColumnId, - slugifiedId: linkColumnId, - columnComponentType: linkComponent.__typename, - formatter: () => "", - baseUrl: links.baseUrl, - linkComponentId: getSlugifiedId(links.componentId), - }; - meta[linkColumnId] = linkColumnMeta; - } - return meta; - }, [ - dimensions, - measures, - fields, - hasLinkColumn, - formatters, - chartData, - links.componentId, - links.baseUrl, - ]); + }, [dimensions, measures, fields, formatters, chartData]); const xScaleTimeRange = useMemo(() => { const xScaleTimeRangeDomain = extent(timeRangeData, (d) => getX(d)) as [ @@ -488,6 +437,7 @@ const useTableState = ( hiddenIds, sortingIds, xScaleTimeRange, + links, ...variables, }; }; From 33257d930150a13049da917257668463964beb8b Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 6 Nov 2025 14:12:05 +0100 Subject: [PATCH 12/17] feat: Allow selecting a target link column --- app/charts/index.ts | 1 + app/charts/table/linked-cell-wrapper.tsx | 10 ++--- app/config-types.ts | 1 + app/configurator/configurator-state/mocks.ts | 7 +++- .../table/configurator/links-section.tsx | 42 ++++++++++++++++++- app/docs/fixtures.ts | 7 +++- app/locales/de/messages.po | 14 ++++--- app/locales/en/messages.po | 14 ++++--- app/locales/fr/messages.po | 14 ++++--- app/locales/it/messages.po | 14 ++++--- app/utils/chart-config/versioning.ts | 1 + 11 files changed, 97 insertions(+), 28 deletions(-) diff --git a/app/charts/index.ts b/app/charts/index.ts index c28630ab6..2f18b8078 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -621,6 +621,7 @@ export const getInitialConfig = ( enabled: false, baseUrl: "", componentId: "", + targetComponentId: "", }, sorting: [], fields: Object.fromEntries( diff --git a/app/charts/table/linked-cell-wrapper.tsx b/app/charts/table/linked-cell-wrapper.tsx index a22b1022f..7d2dd2454 100644 --- a/app/charts/table/linked-cell-wrapper.tsx +++ b/app/charts/table/linked-cell-wrapper.tsx @@ -25,13 +25,12 @@ const useStyles = makeStyles((theme: Theme) => ({ const getLinkHref = ( cell: Cell, baseUrl: string, - linkComponentId: string + componentId: string ): string => { - const slugifiedId = getSlugifiedId(linkComponentId); + const slugifiedId = getSlugifiedId(componentId); const { original } = cell.row; const value = - original[getSlugifiedId(`${linkComponentId}/__iri__`)] ?? - original[slugifiedId]; + original[getSlugifiedId(`${componentId}/__iri__`)] ?? original[slugifiedId]; return value ? `${baseUrl}${encodeURIComponent(value)}` : ""; }; @@ -52,7 +51,8 @@ export const LinkedCellWrapper = ({ links.enabled && links.baseUrl.trim() !== "" && links.componentId.trim() !== "" && - getSlugifiedId(links.componentId) === columnMeta.slugifiedId; + links.targetComponentId.trim() !== "" && + getSlugifiedId(links.targetComponentId) === columnMeta.slugifiedId; const href = getLinkHref(cell, links.baseUrl, links.componentId); if (!isLinkedColumn || !href) { diff --git a/app/config-types.ts b/app/config-types.ts index f58fa635c..9029bae39 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -828,6 +828,7 @@ const TableLinks = t.type({ enabled: t.boolean, baseUrl: t.string, componentId: t.string, + targetComponentId: t.string, }); export type TableLinks = t.TypeOf; diff --git a/app/configurator/configurator-state/mocks.ts b/app/configurator/configurator-state/mocks.ts index 499e6eb28..75ea6d444 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -1299,7 +1299,12 @@ export const configJoinedCubes: Partial< conversionUnitsByComponentId: {}, chartType: "table", settings: { showSearch: true, showAllRows: false }, - links: { enabled: false, baseUrl: "", componentId: "" }, + links: { + enabled: false, + baseUrl: "", + componentId: "", + targetComponentId: "", + }, sorting: [], fields: { joinBy__0: { diff --git a/app/configurator/table/configurator/links-section.tsx b/app/configurator/table/configurator/links-section.tsx index 95d9ef980..56a8f4d91 100644 --- a/app/configurator/table/configurator/links-section.tsx +++ b/app/configurator/table/configurator/links-section.tsx @@ -48,6 +48,33 @@ export const TableLinksSection = ({ value: linkComponentId, }, }); + + if (chartConfig.links.targetComponentId === "") { + dispatch({ + type: "CHART_FIELD_UPDATED", + value: { + locale, + field: null, + path: "links.targetComponentId", + value: linkComponentId, + }, + }); + } + } + ); + + const handleTargetComponentIdChange = useEvent( + (e: SelectChangeEvent) => { + const targetComponentId = e.target.value as string; + dispatch({ + type: "CHART_FIELD_UPDATED", + value: { + locale, + field: null, + path: "links.targetComponentId", + value: targetComponentId, + }, + }); } ); @@ -74,7 +101,7 @@ export const TableLinksSection = ({ size="sm" label={t({ id: "controls.tableSettings.linkComponentId", - message: "Dimension", + message: "Source Dimension", })} options={dimensionOptions} defaultValue={chartConfig.links.componentId} @@ -82,6 +109,19 @@ export const TableLinksSection = ({ disabled={!chartConfig.links.enabled} onChange={handleLinkComponentIdChange} /> +