diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f845c1b..23bed39ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,20 +11,21 @@ You can also check the ## Unreleased -Nothing yet. +- Features + - Added a way to create a link column in table charts ### 6.1.3 - 2025-11-12 - Features - Added dynamic iframe resize support for LivingDocs -- Fix +- Fixes - Clicking on Y axis title in bar charts now open Data tab in the details panels, as in other chart types - Fixed parsing problems with API routes ### 6.1.2 - 2025-10-22 -- Fix +- Fixes - Correctly typed DataSourceUrl variable when querying version history of a cube diff --git a/app/charts/index.ts b/app/charts/index.ts index f0e34d2f0..2f18b8078 100644 --- a/app/charts/index.ts +++ b/app/charts/index.ts @@ -617,6 +617,12 @@ export const getInitialConfig = ( showSearch: true, showAllRows: false, }, + links: { + enabled: false, + baseUrl: "", + componentId: "", + targetComponentId: "", + }, sorting: [], fields: Object.fromEntries( allDimensionsSorted.map((d, i) => [ diff --git a/app/charts/table/cell-desktop.tsx b/app/charts/table/cell-desktop.tsx index a93569fbe..da9eb11e4 100644 --- a/app/charts/table/cell-desktop.tsx +++ b/app/charts/table/cell-desktop.tsx @@ -4,8 +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 { 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"; @@ -59,6 +61,7 @@ export const CellDesktop = ({ barShowBackground, } = columnMeta; const classes = useStyles(); + const { links } = useChartState() as TableChartState; switch (columnMeta.type) { case "text": @@ -77,7 +80,9 @@ export const CellDesktop = ({ }} {...cell.getCellProps()} > - {columnMeta.formatter(cell)} + + {columnMeta.formatter(cell)} + ); case "category": @@ -87,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": @@ -109,7 +116,9 @@ export const CellDesktop = ({ }} {...cell.getCellProps()} > - {columnMeta.formatter(cell)} + + {columnMeta.formatter(cell)} + ); case "bar": @@ -125,39 +134,41 @@ 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, + }} + /> + + + )} + ); default: @@ -177,7 +188,9 @@ export const CellDesktop = ({ }} {...cell.getCellProps()} > - {columnMeta.formatter(cell)} + + {columnMeta.formatter(cell)} + ); } diff --git a/app/charts/table/cell-mobile.tsx b/app/charts/table/cell-mobile.tsx index b84aaee88..cad9dc1e2 100644 --- a/app/charts/table/cell-mobile.tsx +++ b/app/charts/table/cell-mobile.tsx @@ -4,7 +4,8 @@ import { Cell } from "react-table"; import { useChartState } from "@/charts/shared/chart-state"; import { getBarLeftOffset, getBarWidth } from "@/charts/table/cell-desktop"; -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"; @@ -17,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(); @@ -43,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": { @@ -84,62 +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, - }} - /> - - - )} - + > + 0 ? barColorPositive : barColorNegative, + }} + /> + + + )} + + ); } 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..436597355 --- /dev/null +++ b/app/charts/table/linked-cell-wrapper.tsx @@ -0,0 +1,79 @@ +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", + fontWeight: "inherit", + textDecoration: "none", + + "&:hover": { + textDecoration: "underline", + }, + }, +})); + +const getLinkHref = ( + cell: Cell, + baseUrl: string, + componentId: string +): string => { + const slugifiedId = getSlugifiedId(componentId); + const { original } = cell.row; + const iriValue = original[getSlugifiedId(`${componentId}/__iri__`)]; + const rawValue = original[slugifiedId]; + const value = iriValue + ? `${iriValue}`.split("/").pop() || iriValue + : rawValue; + + return `${baseUrl}${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() !== "" && + links.targetComponentId.trim() !== "" && + getSlugifiedId(links.targetComponentId) === columnMeta.slugifiedId; + const href = getLinkHref(cell, links.baseUrl, links.componentId); + + if (!isLinkedColumn || !href) { + return <>{children}; + } + + return ( + + {children} + + + ); +}; diff --git a/app/charts/table/table-state.tsx b/app/charts/table/table-state.tsx index 7bc7c1322..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, @@ -117,6 +118,7 @@ export type TableChartState = CommonChartState & groupingIds: string[]; hiddenIds: string[]; sortingIds: { id: string; desc: boolean }[]; + links: TableLinks; }; const useTableState = ( @@ -127,7 +129,7 @@ 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 hasBar = Object.values(fields).some( @@ -212,7 +214,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 +272,9 @@ const useTableState = ( }, }; }); - }, [orderedTableColumns, chartData, dimensions, measures, formatNumber]); + + return columns; + }, [dimensions, measures, orderedTableColumns, chartData, formatNumber]); // Groupings used by react-table const groupingIds = useMemo( @@ -317,7 +321,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 +410,9 @@ const useTableState = ( }), (v) => v.slugifiedId ); - }, [chartData, dimensions, fields, formatters, measures]); + + return meta; + }, [dimensions, measures, fields, formatters, chartData]); const xScaleTimeRange = useMemo(() => { const xScaleTimeRangeDomain = extent(timeRangeData, (d) => getX(d)) as [ @@ -431,6 +437,7 @@ const useTableState = ( hiddenIds, sortingIds, xScaleTimeRange, + links, ...variables, }; }; 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} diff --git a/app/config-types.ts b/app/config-types.ts index 7d705a9ca..9029bae39 100644 --- a/app/config-types.ts +++ b/app/config-types.ts @@ -817,12 +817,21 @@ 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, + targetComponentId: t.string, +}); +export type TableLinks = t.TypeOf; + const TableFields = t.record(t.string, TableColumn); export type TableFields = t.TypeOf; @@ -840,6 +849,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..75ea6d444 100644 --- a/app/configurator/configurator-state/mocks.ts +++ b/app/configurator/configurator-state/mocks.ts @@ -1299,6 +1299,12 @@ export const configJoinedCubes: Partial< conversionUnitsByComponentId: {}, chartType: "table", settings: { showSearch: true, showAllRows: false }, + links: { + enabled: false, + baseUrl: "", + componentId: "", + targetComponentId: "", + }, sorting: [], fields: { joinBy__0: { 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", } `); }); diff --git a/app/configurator/table/configurator/links-section.tsx b/app/configurator/table/configurator/links-section.tsx new file mode 100644 index 000000000..3be008b11 --- /dev/null +++ b/app/configurator/table/configurator/links-section.tsx @@ -0,0 +1,223 @@ +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, + }, + }); + + 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, + }, + }); + } + ); + + return ( + + + Links + + + + + + + + ); +}; + +const BaseUrlInput = ({ + value, + disabled, +}: { + value: string; + disabled: boolean; +}) => { + const locale = useLocale(); + const [_, dispatch] = useConfiguratorState(isConfiguring); + + const [inputValue, setInputValue] = useState(value); + const [isValid, setIsValid] = useState(true); + + useEffect(() => { + setInputValue(value); + }, [value]); + + const updateBaseUrl = useEvent((newValue: string) => { + dispatch({ + type: "CHART_FIELD_UPDATED", + value: { + locale, + field: null, + path: "links.baseUrl", + value: newValue, + }, + }); + }); + + const handleCommit = useEvent(() => { + if (inputValue === "") { + setIsValid(true); + updateBaseUrl(""); + + return; + } + + const sanitizedUrl = sanitizeUrl(inputValue); + + if (sanitizedUrl === "about:blank") { + setIsValid(false); + updateBaseUrl(""); + + return; + } + + try { + const url = new URL(sanitizedUrl); + const normalizedUrl = normalizeUrl(url); + + updateBaseUrl(normalizedUrl); + setIsValid(true); + setInputValue(normalizedUrl); + } catch { + setIsValid(false); + } + }); + + const handleKeyDown = useEvent((e: KeyboardEvent) => { + if (e.key === "Enter") { + handleCommit(); + } + }); + + return ( + 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/docs/fixtures.ts b/app/docs/fixtures.ts index dd273e52e..8925d874e 100644 --- a/app/docs/fixtures.ts +++ b/app/docs/fixtures.ts @@ -1143,6 +1143,12 @@ export const tableConfig: TableConfig = { }, }, settings: { showSearch: true, showAllRows: true }, + links: { + enabled: false, + baseUrl: "", + componentId: "", + targetComponentId: "", + }, sorting: [ { componentId: diff --git a/app/locales/de/messages.po b/app/locales/de/messages.po index bdec3b4c1..866f2ff44 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" @@ -1129,6 +1133,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 +1408,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 +1752,10 @@ msgstr "Legendentitel anzeigen" msgid "show.less" msgstr "Weniger anzeigen" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.showLinks" +msgstr "Links aktivieren" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Zeig mehr" @@ -1835,6 +1851,10 @@ msgstr "Die Sortierung nach Gesamtgrösse ist während der Animation deaktiviert msgid "dataset.metadata.source" msgstr "Quelle" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Quellspalte" + #: app/components/dataset-metadata.tsx msgid "chart-controls.sparql-query" msgstr "SPARQL-Abfrage" @@ -1921,6 +1941,10 @@ msgstr "Tabellenansicht" msgid "controls.layout.tall" msgstr "Hoch" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.targetComponentId" +msgstr "Zielspalte" + #: app/configurator/components/chart-options-selector/limits-field.tsx msgid "controls.section.targets-and-limit-values" msgstr "Zielvorgaben und Grenzwerte" diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po index 01b1bf31b..4835c2f2a 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" @@ -1129,6 +1133,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 +1408,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 +1752,10 @@ msgstr "Show legend titles" msgid "show.less" msgstr "Show less" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.showLinks" +msgstr "Enable links" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Show more" @@ -1835,6 +1851,10 @@ msgstr "Sorting by total size is disabled during animation." msgid "dataset.metadata.source" msgstr "Source" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Source Column" + #: app/components/dataset-metadata.tsx msgid "chart-controls.sparql-query" msgstr "SPARQL query" @@ -1921,6 +1941,10 @@ msgstr "Table view" msgid "controls.layout.tall" msgstr "Tall" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.targetComponentId" +msgstr "Target Column" + #: app/configurator/components/chart-options-selector/limits-field.tsx msgid "controls.section.targets-and-limit-values" msgstr "Targets & limit values" diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po index 30cbfc895..d4a94c9e0 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" @@ -1129,6 +1133,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 +1408,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 +1752,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 "Activer les liens" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Détails" @@ -1835,6 +1851,10 @@ msgstr "Le tri par taille totale est désactivé pendant l'animation." msgid "dataset.metadata.source" msgstr "Source" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Colonne source" + #: app/components/dataset-metadata.tsx msgid "chart-controls.sparql-query" msgstr "Requête SPARQL" @@ -1921,6 +1941,10 @@ msgstr "Vue en tableau" msgid "controls.layout.tall" msgstr "Haut" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.targetComponentId" +msgstr "Colonne cible" + #: app/configurator/components/chart-options-selector/limits-field.tsx msgid "controls.section.targets-and-limit-values" msgstr "Objectifs et valeurs limites" diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po index e2c5e1c3c..9820b1421 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" @@ -1129,6 +1133,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 +1408,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 +1752,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 "Abilita i collegamenti" + #: app/components/metadata-panel.tsx msgid "controls.metadata-panel.show-more" msgstr "Mostra di più" @@ -1835,6 +1851,10 @@ msgstr "L'ordinamento per dimensione totale è disabilitato durante l'animazione msgid "dataset.metadata.source" msgstr "Fonte" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.linkComponentId" +msgstr "Colonna di origine" + #: app/components/dataset-metadata.tsx msgid "chart-controls.sparql-query" msgstr "SPARQL query" @@ -1921,6 +1941,10 @@ msgstr "Visualizzazione tabella" msgid "controls.layout.tall" msgstr "Alto" +#: app/configurator/table/configurator/links-section.tsx +msgid "controls.tableSettings.targetComponentId" +msgstr "Colonna di destinazione" + #: app/configurator/components/chart-options-selector/limits-field.tsx msgid "controls.section.targets-and-limit-values" msgstr "Obiettivi e valori limite" 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/app/themes/components.tsx b/app/themes/components.tsx index d0fb5b47a..728c13c34 100644 --- a/app/themes/components.tsx +++ b/app/themes/components.tsx @@ -506,6 +506,7 @@ export const components: Components = { "&::placeholder": { opacity: 1, + color: palette.text.secondary, }, }, }, 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..077555217 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -1638,6 +1638,40 @@ 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: "", + targetComponentId: "", + }; + } + + return newConfig; + }, + down: (config) => { + const newConfig = { ...config, version: "5.0.0" }; + + if (newConfig.chartType === "table") { + delete newConfig.links; + } + return newConfig; }, }, @@ -2272,6 +2306,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( 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"